一个对象的属性_【前端冷知识】如何判断一个对象的某个属性是可写的?
這是一個咋一聽好像很簡單,但是實際上卻沒那么簡單,而且是很有趣的問題。
我們先來看一下什么情況下一個對象的屬性是可寫的。
“屬性可寫”這個概念并沒有嚴謹的定義,我們這里先來規定一下。
屬性可寫,是指滿足如下條件:
對于任意對象object,該對象的a屬性可寫,是指如下代碼成立:
const?value?=?Symbol();
object.a?=?value;
console.assert(obj.a?===?value);
JavaScript有幾種情況下,對象屬性不可寫。
?? 第一種情況,如果這個屬性是accessor property,并且只有一個getter時,這個屬性不可寫。
const?obj?=?{
??get?a(){
????return?'a';
??}
};
console.log(obj.a);?// a
obj.a?=?'b';
console.log(obj.a);?// a
?? 第二種情況,如果這個屬性的Descriptor中設置了writable為false,這個屬性不可寫。
const?obj?=?{};
Object.defineProperty(obj,?'a',?{
??value:?'a',
??writable:?false,
});
console.log(obj.a);?// a
obj.a?=?'b';
console.log(obj.a);?// a
?? 第三種情況,目標對象被Object.freeze,實際上也是將對象上所有屬性的writable設為了false:
const?obj?=?{a:?'a'};
Object.freeze(obj);
console.log(obj.a);?// a
obj.a?=?'b';
console.log(obj.a);?// a
那么了解了這些情況,我們就可以嘗試寫一個方法來判斷對象屬性是否可寫了:
function?isOwnPropertyWritable(obj,?prop)?{
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
上面這個方法可以簡單判斷一個對象自身的屬性是否可寫,判斷邏輯也不復雜,先通過Object.getOwnPropertyDescriptor(obj, prop)方法獲取對象自身屬性的Descriptor,接下來有三種情況對象的這個屬性可寫:
這個Descriptor不存在,表示對象上沒有該屬性,那么我們可以動態添加這個屬性
這個Descriptor存在,且writable為true,那么屬性可寫
這個Descriptor存在,且擁有getter,那么屬性可寫
看似好像解決了這個問題,但是,實際上這個判斷有很多問題。
首先,最大的問題是,這個方法只能判斷對象自身的屬性,如果對象原型和原型鏈上的屬性,實際上getOwnPropertyDescriptor是訪問不到的,我們看一個簡單例子:
function?isOwnPropertyWritable(obj,?prop)?{
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
class?A?{
??get?a()?{
????return?'a';
??}
}
const?obj?=?new?A();
console.log(isOwnPropertyWritable(obj,?'a'));?// true
console.log(obj.a);?// a
obj.a?=?'b';
console.log(obj.a);?// a
上面的代碼,我們預期的isOwnPropertyWritable(obj, 'a')應該返回false,但實際上卻是返回true,這是因為Object.getOwnPropertyDescriptor獲取不到class中定義的getter,該getter實際上是在obj的原型上。
要解決這個問題,我們需要沿原型鏈遞歸判斷屬性:
function?isPropertyWritable(obj,?prop)?{
??while(obj)?{
????if(!isOwnPropertyWritable(obj,?prop))?return?false;
????obj?=?Object.getPrototypeOf(obj);
??}
??return?true;
}
我們實現一個isPropertyWritable(obj, prop),不僅判斷自身,也判斷一下它的原型鏈。
這樣我們就解決了繼承屬性的問題。
function?isOwnPropertyWritable(obj,?prop)?{
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
function?isPropertyWritable(obj,?prop)?{
??while(obj)?{
????if(!isOwnPropertyWritable(obj,?prop))?return?false;
????obj?=?Object.getPrototypeOf(obj);
??}
??return?true;
}
class?A?{
??get?a()?{
????return?'a';
??}
}
class?B?extends?A?{
}
const?a?=?new?A();
const?b?=?new?B();
console.log(isPropertyWritable(a,?'a'));?// false
console.log(isPropertyWritable(b,?'a'));?// false
但是實際上這樣實現還是有缺陷,我們其實還少了幾個情況。
首先,我們處理原始類型,比如現在下面的代碼會有問題:
const?obj?=?1;
obj.a?=?'a';
console.log(isPropertyWritable(obj,?'a'));?// true
console.log(obj.a);?// undefined
所以我們要修改一下isOwnPropertyWritable的實現:
function?isOwnPropertyWritable(obj,?prop)?{
??if(obj?==?null)?return?false;
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
然后,其實還有一些case,比如:
function?isOwnPropertyWritable(obj,?prop)?{
??if(obj?==?null)?return?false;
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
function?isPropertyWritable(obj,?prop)?{
??// noprotected
??while(obj)?{
????if(!isOwnPropertyWritable(obj,?prop))?return?false;
????obj?=?Object.getPrototypeOf(obj);
??}
??return?true;
}
const?obj?=?{};
Object.seal(obj);
console.log(isPropertyWritable(obj,?'a'));?// true
obj.a?=?'b';
console.log(obj.a);?// undefined
我們還需要考慮seal的情況。
?? Object.seal 方法封閉一個對象,阻止添加新屬性并將所有現有屬性標記為不可配置。
所以對這種情況我們也要加以判斷:
function?isOwnPropertyWritable(obj,?prop)?{
??if(obj?==?null)?return?false;
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??if(!(prop?in?obj)?&&?Object.isSealed(obj))?return?false;
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
好了,那最后得到的版本就是這樣的:
function?isOwnPropertyWritable(obj,?prop)?{
??// 判斷 null 和 undefined
??if(obj?==?null)?return?false;
??// 判斷其他原始類型
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??// 判斷sealed的新增屬性
??if(!(prop?in?obj)?&&?Object.isSealed(obj))?return?false;
??// 判斷屬性描述符
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
function?isPropertyWritable(obj,?prop)?{
??while(obj)?{
????if(!isOwnPropertyWritable(obj,?prop))?return?false;
????obj?=?Object.getPrototypeOf(obj);
??}
??return?true;
}
這樣就100%沒問題了嗎?
也不是,嚴格來說,我們還是可以trick,比如給對象故意設一個setter:
function?isOwnPropertyWritable(obj,?prop)?{
??// 判斷 null 和 undefined
??if(obj?==?null)?return?false;
??// 判斷其他原始類型
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??// 判斷sealed的新增屬性
??if(!(prop?in?obj)?&&?Object.isSealed(obj))?return?false;
??// 判斷屬性描述符
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
function?isPropertyWritable(obj,?prop)?{
??while(obj)?{
????if(!isOwnPropertyWritable(obj,?prop))?return?false;
????obj?=?Object.getPrototypeOf(obj);
??}
??return?true;
}
const?obj?=?{
??get?a()?{
????return?'a';
??},
??set?a(v)?{
????// do nothing
??}
}
console.log(isPropertyWritable(obj,?'a'));?// true
obj.a?=?'b';
console.log(obj.a);?// a
你可能會說,這種trick太無聊了,但是事實上類似下面的代碼還是有可能寫出來的:
const?obj?=?{
??name:?'a',
??get?a()?{
????return?this.name;
??},
??set?a(v)?{
????this.name?=?v;
??}
};
Object.freeze(obj);
console.log(isPropertyWritable(obj,?'a'));
當然要解決這個問題也不是不可以,還要加上一個判斷:
function?isOwnPropertyWritable(obj,?prop)?{
??// 判斷 null 和 undefined
??if(obj?==?null)?return?false;
??// 判斷其他原始類型
??const?type?=?typeof?obj;
??if(type?!==?'object'?&&?type?!==?'function')?return?false;
??// 判斷是否被凍結
??if(Object.isFrozen(obj))?return?false;
??// 判斷sealed的新增屬性
??if(!(prop?in?obj)?&&?Object.isSealed(obj))?return?false;
??// 判斷屬性描述符
??const?des?=?Object.getOwnPropertyDescriptor(obj,?prop);
??return?des?==?null?||?des.writable?||?!!des.set;
}
所以,要考慮的情況著實不少,也不知道還有沒有沒考慮周全的。
有可能還真得換一個思路,從定義入手:
function?isPropertyWritable(obj,?prop)?{
??const?value?=?obj[prop];
??const?sym?=?Symbol();
??try?{
????obj[prop]?=?sym;
??}?catch(ex)?{
????// 解決在嚴格模式下報錯問題
????return?false;
??}
??const?isWritable?=?obj[prop]?===?sym;
??obj[prop]?=?value;?// 恢復原來的值
??return?isWritable;
}
這樣就解決了問題,唯一的問題是對屬性做了兩次賦值操作,不過應該也沒有太大的關系。
補充:經過大家討論,上面這個思路也不行,如果屬性的setter中執行一些操作,會有很大的問題,比如我們observe一些對象,用這個方法因為寫入了兩次,可能會觸發兩次change事件。。。
所以基本上運行時判斷某個屬性可寫,沒有特別好的手段,也許只能使用TypeScript這樣的靜態類型語言在編譯時檢查,才是比較好的方案~
好了,關于判斷對象屬性是否可寫的方法,你還有什么問題,歡迎在issue中討論。
關于奇舞周刊
《奇舞周刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公眾號后,直接發送鏈接到后臺即可給我們投稿。
總結
以上是生活随笔為你收集整理的一个对象的属性_【前端冷知识】如何判断一个对象的某个属性是可写的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python字符计数怎样去除空格_去除p
- 下一篇: 传递list对象作为参数_24.scal