寫于 2017年08月20日
,雖然是2017年寫的文章,但現在即將2020年
依舊不過時,現在補充了2019年
新增的ES10
Object.fromEntries()
。發到公眾號申明原創。若川順便在此提前祝大家:2020年更上一層樓。
近日發現有挺多人對對象基礎API
不熟悉,舉個開發中常見的需求,經常會有類似的封裝http
到原型Vue.prototype
,一般人是這樣封裝的,但容易被篡改。
function Vue(){console.log('test vue');
}
function http(){console.log('我是調用接口的http');
}
Vue.prototype.$http = http;
var vm = new Vue();
vm.$http()
vm.$http = 1; // 一旦被修改,雖然一般正常情況下不會被修改
vm.$http(); // 再次調用報錯
熟悉Object.defineProperty
或者說熟悉對象API
的人,一般是如下代碼寫的,則不會出現被修改的問題。
function Vue(){console.log('test vue');
};
function http(){console.log('我是調用接口的http');
};
Object.defineProperty(Vue.prototype, '$http', {get(){return http;}
});
var vm = new Vue();
vm.$http();
vm.$http = 1; // 這里無法修改
vm.$http(); // 調用正常
vue-router 源碼里就是類似這樣寫的[1],this.$router
,this.$route
無法修改。
// vue-router 源碼
Object.defineProperty(Vue.prototype, '$router', {get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {get () { return this._routerRoot._route }
})
以下是正文,祝閱讀愉快~
之前看到【深度長文】JavaScript 數組所有 API 全解密[2]和JavaScript 字符串所有 API 全解密[3]這兩篇高質量的文章。發現沒寫對象 API 解析(估計是博主覺得簡單,就沒寫)。剛好我看到《JavaScript 面向對象編程指南(第 2 版)》,覺得有必要寫(或者說 chao)一下,也好熟悉下對象的所有 API 用法。
創建對象的兩種方式:
var o = new Object();
var o = {}; // 推薦
該構造器可以接受任何類型的參數,并且會自動識別參數的類型,并選擇更合適的構造器來完成相關操作。比如:
var o = new Object('something');
o.constructor; // ? String() { [native code] }
var n = new Object(123);
n.constructor; // ? Number() { [native code] }
一、Object 構造器的成員
Object.prototype
該屬性是所有對象的原型(包括 Object
對象本身),語言中的其他對象正是通過對該屬性上添加東西來實現它們之間的繼承關系的。所以要小心使用。比如:
var s = new String('若川');
Object.prototype.custom = 1;
console.log(s.custom); // 1
二、Object.prototype 的成員
Object.prototype.constructor
該屬性指向用來構造該函數對象的構造器,在這里為Object()
Object.prototype.constructor === Object; // true
var o = new Object();
o.constructor === Object; // true
Object.prototype.toString(radix)
該方法返回的是一個用于描述目標對象的字符串。特別地,當目標是一個 Number 對象時,可以傳遞一個用于進制數的參數radix
,該參數radix
,該參數的默認值為 10。
var o = {prop:1};
o.toString(); // '[object Object]'
var n = new Number(255);
n.toString(); // '255'
n.toString(16); // 'ff'
Object.prototype.toLocaleString()
該方法的作用與toString()
基本相同,只不過它做一些本地化處理。該方法會根據當前對象的不同而被重寫,例如Date()
,Number()
,Array()
,它們的值都會以本地化的形式輸出。當然,對于包括Object()
在內的其他大多數對象來說,該方法與toString()
是基本相同的。在瀏覽器環境下,可以通過BOM
對象Navigator
的language
屬性(在IE
中則是userLanguage
)來了解當前所使用的語言:
navigator.language; //'en-US'
Object.prototype.valueOf()
該方法返回的是用基本類型所表示的this
值,如果它可以用基本類型表示的話。如果Number
對象返回的是它的基本數值,而Date
對象返回的是一個時間戳(timestamp
)。如果無法用基本數據類型表示,該方法會返回this
本身。
// Object
var o = {};
typeof o.valueOf(); // 'object'
o.valueOf() === o; // true
// Number
var n = new Number(101);
typeof n; // 'object'
typeof n.vauleOf; // 'function'
typeof n.valueOf(); // 'number'
n.valueOf() === n; // false
// Date
var d = new Date();
typeof d.valueOf(); // 'number'
d.valueOf(); // 1503146772355
Object.prototype.hasOwnProperty(prop)
該方法僅在目標屬性為對象自身屬性時返回true
,而當該屬性是從原型鏈中繼承而來或根本不存在時,返回false
。
var o = {prop:1};
o.hasOwnProperty('prop'); // true
o.hasOwnProperty('toString'); // false
o.hasOwnProperty('formString'); // false
Object.prototype.isPrototypeOf(obj)
如果目標對象是當前對象的原型,該方法就會返回true
,而且,當前對象所在原型上的所有對象都能通過該測試,并不局限與它的直系關系。
var s = new String('');
Object.prototype.isPrototypeOf(s); // true
String.prototype.isPrototypeOf(s); // true
Array.prototype.isPrototypeOf(s); // false
Object.prototype.propertyIsEnumerable(prop)
如果目標屬性能在for in
循環中被顯示出來,該方法就返回true
var a = [1,2,3];
a.propertyIsEnumerable('length'); // false
a.propertyIsEnumerable(0); // true
三、在ES5
中附加的Object
屬性
在ES3
中,除了一些內置屬性(如:Math.PI
),對象的所有的屬性在任何時候都可以被修改、插入、刪除。在ES5
中,我們可以設置屬性是否可以被改變或是被刪除——在這之前,它是內置屬性的特權。ES5
中引入了屬性描述符的概念,我們可以通過它對所定義的屬性有更大的控制權。這些屬性描述符(特性)包括:
value
——當試圖獲取屬性時所返回的值。writable
——該屬性是否可寫。enumerable
——該屬性在for in
循環中是否會被枚舉configurable
——該屬性是否可被刪除。set()
——該屬性的更新操作所調用的函數。get()
——獲取屬性值時所調用的函數。另外,數據描述符(其中屬性為:enumerable
,configurable
,value
,writable
)與存取描述符(其中屬性為enumerable
,configurable
,set()
,get()
)之間是有互斥關系的。在定義了set()
和get()
之后,描述符會認為存取操作已被 定義了,其中再定義value
和writable
會引起錯誤。以下是ES3風格的屬性定義方式:
var person = {};
person.legs = 2;
以下是等價的 ES5 通過數據描述符定義屬性的方式:
var person = {};
Object.defineProperty(person, 'legs', {value: 2,writable: true,configurable: true,enumerable: true
});
其中, 除了 value 的默認值為undefined
以外,其他的默認值都為false
。這就意味著,如果想要通過這一方式定義一個可寫的屬性,必須顯示將它們設為true
。或者,我們也可以通過ES5
的存儲描述符來定義:
var person = {};
Object.defineProperty(person, 'legs', {set:function(v) {return this.value = v;},get: function(v) {return this.value;},configurable: true,enumerable: true
});
person.legs = 2;
這樣一來,多了許多可以用來描述屬性的代碼,如果想要防止別人篡改我們的屬性,就必須要用到它們。此外,也不要忘了瀏覽器向后兼容ES3
方面所做的考慮。例如,跟添加Array.prototype
屬性不一樣,我們不能在舊版的瀏覽器中使用shim
這一特性。另外,我們還可以(通過定義nonmalleable
屬性),在具體行為中運用這些描述符:
var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1 (改不了)
delete person.heads; // false
person.heads // 1 (刪不掉)
Object.defineProperty(obj, prop, descriptor) (ES5)
具體用法可參見上文,或者查看 MDN。MDN Object.defineProperty(obj, descriptor)[4]
Vue.js 文檔:**如何追蹤變化**[5] 把一個普通 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。Object.defineProperty 是僅 ES5 支持,且無法 shim 的特性,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
Object.defineProperties(obj, props) (ES5)
該方法的作用與defineProperty()
基本相同,只不過它可以用來一次定義多個屬性。比如:
var glass = Object.defineProperties({}, {'color': {value: 'transparent',writable: true},'fullness': {value: 'half',writable: false}
});
glass.fullness; // 'half'
Object.getPrototypeOf(obj) (ES5)
之前在ES3
中,我們往往需要通過Object.prototype.isPrototypeOf()
去猜測某個給定的對象的原型是什么,如今在ES5
中,我們可以直接詢問改對象“你的原型是什么?”
Object.getPrototypeOf([]) === Array.prototype; // true
Object.getPrototypeOf(Array.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype) === null; // true
Object.create(obj, descr) (ES5)
該方法主要用于創建一個新對象,并為其設置原型,用(上述)屬性描述符來定義對象的原型屬性。
var parent = {hi: 'Hello'};
var o = Object.create(parent, {prop: {value: 1}
});
o.hi; // 'Hello'
// 獲得它的原型
Object.getPrototypeOf(parent) === Object.prototype; // true 說明parent的原型是Object.prototype
Object.getPrototypeOf(o); // {hi: "Hello"} // 說明o的原型是{hi: "Hello"}
o.hasOwnProperty('hi'); // false 說明hi是原型上的
o.hasOwnProperty('prop'); // true 說明prop是原型上的自身上的屬性。
現在,我們甚至可以用它來創建一個完全空白的對象,這樣的事情在ES3
中可是做不到的。
var o = Object.create(null);
typeof o.toString(); // 'undefined'
Object.getOwnPropertyDesciptor(obj, property) (ES5)
該方法可以讓我們詳細查看一個屬性的定義。甚至可以通過它一窺那些內置的,之前不可見的隱藏屬性。
Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// {writable: true, enumerable: false, configurable: true, value: ? toString()}
Object.getOwnPropertyNames(obj) (ES5)
該方法返回一個數組,其中包含了當前對象所有屬性的名稱(字符串),不論它們是否可枚舉。當然,也可以用Object.keys()
來單獨返回可枚舉的屬性。
Object.getOwnPropertyNames(Object.prototype);
// ["__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "propertyIsEnumerable", "toString", "valueOf", "__proto__", "constructor", "toLocaleString", "isPrototypeOf"]
Object.keys(Object.prototype);
// []
Object.getOwnPropertyNames(Object);
// ["length", "name", "arguments", "caller", "prototype", "assign", "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", "getOwnPropertyNames", "getOwnPropertySymbols", "is", "preventExtensions", "seal", "create", "defineProperties", "defineProperty", "freeze", "getPrototypeOf", "setPrototypeOf", "isExtensible", "isFrozen", "isSealed", "keys", "entries", "values"]
Object.keys(Object);
// []
Object.preventExtensions(obj) (ES5)
Object.isExtensible(obj) (ES5)
preventExtensions()
方法用于禁止向某一對象添加更多屬性,而isExtensible()
方法則用于檢查某對象是否還可以被添加屬性。
var deadline = {};
Object.isExtensible(deadline); // true
deadline.date = 'yesterday'; // 'yesterday'
Object.preventExtensions(deadline);
Object.isExtensible(deadline); // false
deadline.date = 'today';
deadline.date; // 'today'
// 盡管向某個不可擴展的對象中添加屬性不算是一個錯誤操作,但它沒有任何作用。
deadline.report = true;
deadline.report; // undefined
Object.seal(obj) (ES5)
Object.isSeal(obj) (ES5)
seal()
方法可以讓一個對象密封,并返回被密封后的對象。seal()
方法的作用與preventExtensions()
基本相同,但除此之外,它還會將現有屬性 設置成不可配置。也就是說,在這種情況下,我們只能變更現有屬性的值,但不能刪除或(用defineProperty()
)重新配置這些屬性,例如不能將一個可枚舉的屬性改成不可枚舉。
var person = {legs:2};
// person === Object.seal(person); // true
Object.isSealed(person); // true
Object.getOwnPropertyDescriptor(person, 'legs');
// {value: 2, writable: true, enumerable: true, configurable: false}
delete person.legs; // false (不可刪除,不可配置)
Object.defineProperty(person, 'legs',{value:2});
person.legs; // 2
person.legs = 1;
person.legs; // 1 (可寫)
Object.defineProperty(person, "legs", { get: function() { return "legs"; } });
// 拋出TypeError異常
Object.freeze(obj) (ES5)
Object.isFrozen(obj) (ES5)
freeze()
方法用于執行一切不受seal()
方法限制的屬性值變更。Object.freeze()
方法可以凍結一個對象,凍結指的是不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。也就是說,這個對象永遠是不可變的。該方法返回被凍結的對象。
var deadline = Object.freeze({date: 'yesterday'});
deadline.date = 'tomorrow';
deadline.excuse = 'lame';
deadline.date; // 'yesterday'
deadline.excuse; // undefined
Object.isSealed(deadline); // true;
Object.isFrozen(deadline); // true
Object.getOwnPropertyDescriptor(deadline, 'date');
// {value: "yesterday", writable: false, enumerable: true, configurable: false} (不可配置,不可寫)
Object.keys(deadline); // ['date'] (可枚舉)
Object.keys(obj) (ES5)
該方法是一種特殊的for-in
循環。它只返回當前對象的屬性(不像for-in
),而且這些屬性也必須是可枚舉的(這點和Object.getOwnPropertyNames()
不同,不論是否可以枚舉)。返回值是一個字符串數組。
Object.prototype.customProto = 101;
Object.getOwnPropertyNames(Object.prototype);
// [..., "constructor", "toLocaleString", "isPrototypeOf", "customProto"]
Object.keys(Object.prototype); // ['customProto']
var o = {own: 202};
o.customProto; // 101
Object.keys(o); // ['own']
四、在ES6
中附加的Object
屬性
Object.is(value1, value2) (ES6)
該方法用來比較兩個值是否嚴格相等。它與嚴格比較運算符(===)的行為基本一致。不同之處只有兩個:一是+0
不等于-0
,而是NaN
等于自身。
Object.is('若川', '若川'); // true
Object.is({},{}); // false
Object.is(+0, -0); // false
+0 === -0; // true
Object.is(NaN, NaN); // true
NaN === NaN; // false
ES5
可以通過以下代碼部署Object.is
Object.defineProperty(Object, 'is', {value: function() {x, y} {if (x === y) {// 針對+0不等于-0的情況return x !== 0 || 1 / x === 1 / y;}// 針對 NaN的情況return x !== x && y !== y;},configurable: true,enumerable: false,writable: true
});
Object.assign(target, ...sources) (ES6)
該方法用來源對象(source
)的所有可枚舉的屬性復制到目標對象(target
)。它至少需要兩個對象作為參數,第一個參數是目標對象target
,后面的參數都是源對象(source
)。只有一個參數不是對象,就會拋出TypeError
錯誤。
var target = {a: 1};
var source1 = {b: 2};
var source2 = {c: 3};
obj = Object.assign(target, source1, source2);
target; // {a:1,b:2,c:3}
obj; // {a:1,b:2,c:3}
target === obj; // true
// 如果目標對象與源對象有同名屬性,或多個源對象有同名屬性,則后面的屬性會覆蓋前面的屬性。
var source3 = {a:2,b:3,c:4};
Object.assign(target, source3);
target; // {a:2,b:3,c:4}
Object.assign
只復制自身屬性,不可枚舉的屬性(enumerable
為false
)和繼承的屬性不會被復制。
Object.assign({b: 'c'},Object.defineProperty({}, 'invisible', {enumerable: false,value: 'hello'})
);
// {b: 'c'}
屬性名為Symbol
值的屬性,也會被Object.assign()
復制。
Object.assign({a: 'b'}, {[Symbol('c')]: 'd'});
// {a: 'b', Symbol(c): 'd'}
對于嵌套的對象,Object.assign()
的處理方法是替換,而不是添加。
Object.assign({a: {b:'c',d:'e'}}, {a:{b:'hello'}});
// {a: {b:'hello'}}
對于數組,Object.assign()
把數組視為屬性名為 0、1、2 的對象。
Object.assign([1,2,3], [4,5]);
// [4,5,3]
Object.getOwnPropertySymbols(obj) (ES6)
該方法會返回一個數組,該數組包含了指定對象自身的(非繼承的)所有 symbol
屬性鍵。該方法和 Object.getOwnPropertyNames()
類似,但后者返回的結果只會包含字符串類型的屬性鍵,也就是傳統的屬性名。
Object.getOwnPropertySymbols({a: 'b', [Symbol('c')]: 'd'});
// [Symbol(c)]
Object.setPrototypeOf(obj, prototype) (ES6)
該方法設置一個指定的對象的原型 ( 即, 內部[[Prototype]]
屬性)到另一個對象或 null
。__proto__
屬性用來讀取或設置當前對象的prototype
對象。目前,所有瀏覽器(包括IE11
)都部署了這個屬性。
// ES6寫法
var obj = {method: function(){// code ...}
};
// obj.__proto__ = someOtherObj;
// ES5寫法
var obj = Object.create(someOtherObj);
obj.method = function(){// code ...
};
該屬性沒有寫入ES6
的正文,而是寫入了附錄。__proto__
前后的雙下劃線說明它本質上是一個內部屬性,而不是正式對外的一個 API。無論從語義的角度,還是從兼容性的角度,都不要使用這個屬性。而是使用Object.setPrototypeOf()
(寫操作),Object.getPrototypeOf()
(讀操作),或Object.create()
(生成操作)代替。在實現上,__proto__
調用的Object.prototype.__proto__
。Object.setPrototypeOf()
方法的作用與__proto__
作用相同,用于設置一個對象的prototype
對象。它是ES6
正式推薦的設置原型對象的方法。
五、在ES8
中附加的Object
屬性
Object.getOwnPropertyDescriptors(obj) (ES8)
該方法基本與Object.getOwnPropertyDescriptor(obj, property)
用法一致,只不過它可以用來獲取一個對象的所有自身屬性的描述符。
Object.getOwnPropertyDescriptor(Object.prototype, 'toString');
// {writable: true, enumerable: false, configurable: true, value: ? toString()}
Object.getOwnPropertyDescriptors(Object.prototype); // 可以自行在瀏覽器控制臺查看效果。
Object.values(obj) (ES8)
Object.values()
方法與Object.keys
類似。返回一個給定對象自己的所有可枚舉屬性值的數組,值的順序與使用for...in
循環的順序相同 ( 區別在于for-in
循環枚舉原型鏈中的屬性 )。
var obj = {a:1,b:2,c:3};
Object.keys(obj); // ['a','b','c']
Object.values(obj); // [1,2,3]
Object.entries(obj) (ES8)
Object.entries()
方法返回一個給定對象自己的可枚舉屬性[key,value]
對的數組,數組中鍵值對的排列順序和使用 for...in
循環遍歷該對象時返回的順序一致(區別在于一個for-in
循環也枚舉原型鏈中的屬性)。
var obj = {a:1,b:2,c:3};
Object.keys(obj); // ['a','b','c']
Object.values(obj); // [1,2,3]
Object.entries(obj); // [['a',1],['b',2],['c',3]]
六、在ES10
中附加的Object
屬性
Object.fromEntries(iterable) (ES10)
Object.fromEntries()
方法返回一個給定可迭代對象(類似Array
、Map
或其他可迭代對象)對應屬性的新對象。
Object.fromEntries()
是 Object.entries()
的逆操作。
var arr = [['a',1],['b',2],['c',3]];
Object.fromEntries(obj); // {a: 1, b: 2, c: 3}
var entries = new Map([['name', '若川'],['age', 18]
]);
Object.fromEntries(entries) // {name: '若川', age: 18}
小結
細心的讀者可能會發現MDN
上還有一些API
,本文沒有列舉到。因為那些是非標準的API
。熟悉對象的 API 對理解原型和原型鏈相關知識會有一定幫助。常用的 API 主要有Object.prototype.toString()
,Object.prototype.hasOwnProperty()
, Object.getPrototypeOf(obj)
,Object.create()
,Object.defineProperty
,Object.keys(obj)
,Object.assign()
。
如果讀者發現有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支持,非常感謝呀。
參考資料
MDN Object API[6]
JavaScript 面向對象編程指南(第 2 版)(豆瓣讀書鏈接)[7]
阮一峰 ES6 標準入門 2[8]
原創精選文章
工作一年后,我有些感悟(寫于2017年)
高考七年后、工作三年后的感悟
面試官問:JS的繼承
前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并
學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫
學習underscore源碼整體架構,打造屬于自己的函數式編程類庫
學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫
學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK
學習 vuex 源碼整體架構,打造屬于自己的狀態管理庫
學習 axios 源碼整體架構,打造屬于自己的請求庫
知乎問答:一年內的前端看不懂前端框架源碼怎么辦?
微信公眾號
作者:常以若川為名混跡于江湖。前端路上 | PPT 愛好者 | 所知甚少,唯善學。
博客:https://lxchuan12.cn/posts/
,閱讀體驗可能更好些。
主要發布
前端 | PPT | 生活 | 效率
相關的文章,長按掃碼關注。歡迎加我微信lxchuan12
(注明來源,基本來者不拒),拉您進【前端視野交流群】,長期交流學習~
參考資料
[1]
vue-router 源碼里就是類似這樣寫的: https://github.com/vuejs/vue-router/blob/dev/src/install.js#L38-L44
[2]【深度長文】JavaScript數組所有API全解密: http://louiszhai.github.io/2017/04/28/array/
[3]JavaScript字符串所有API全解密: http://louiszhai.github.io/2016/01/12/js.String/
[4]MDN Object.defineProperty(obj, descriptor): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
[5]如何追蹤變化: https://cn.vuejs.org/v2/guide/reactivity.html
[6]MDN Object API: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
[7]JavaScript面向對象編程指南(第2版)(豆瓣讀書鏈接): https://book.douban.com/subject/26302623/
[8]阮一峰 ES6標準入門2: http://es6.ruanyifeng.com/
由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^