本文原載自 http://js-professional.lxfriday.xyz/blog/2019/12/31/JavaScript%E4%B8%AD%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF,做爲學習筆記總結呈現。前端
{}
對象字面量Object()
或者 new Object()
new Constructor()
Object.create()
Object.assign()
關於 new Constructor()
Object.create()
和 Object.assign()
建立對象的過程和模擬實現能夠參考這篇文章 前端面試必備 | 5000字長文解釋千萬不能錯過的原型操做方法及其模擬實現(原型篇:下)。面試
function createPerson(name, age, job) { const o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; } const person1 = createPerson("Nicholas", 29, "Software Engineer"); const person2 = createPerson("Greg", 27, "Doctor");
每一次調用上面的 createPerson
工廠函數均可以建立一個對象,這個對象有 name
age
job
三個屬性和一個 sayName
方法,依據傳入的參數的不一樣,返回對象的值也會不一樣。數組
缺點:沒有解決這個對象是一個什麼類型的對象(沒有更精確的對象標識,即沒有精確的構造函數)。瀏覽器
將工廠改形成構造函數以後,以下閉包
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); }; } const person1 = new Person("Nicholas", 29, "Software Engineer"); const person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); // Nicholas person2.sayName(); // Greg
構造函數和工廠的區別:函數
this
return
使用構造函數建立對象將會有如下幾個步驟:佈局
[[Prototype]]
指針指向構造函數的 prototype
屬性指向的對象;this
指向新建立的對象;return
非 null
的對象,那返回的就是這個對象,不然返回新建立的這個對象。沒有 return
時,隱式返回新建立的對象,return null
會返回新建立的對象;缺點:每次實例化一個新對象,都會在內部建立一個 sayName
對應的匿名函數,而這個函數對全部實例來說是沒有必要每次都建立的,他們只須要指向同一個函數便可。學習
因此上面的代碼通過改造以後,變成下面這樣:this
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName() { console.log(this.name); } const person1 = new Person("Nicholas", 29, "Software Engineer"); const person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); // Nicholas person2.sayName(); // Greg
上述的作法雖然解決了重複建立匿名函數的問題,可是又引入了新的問題。spa
外面的 sayName
函數僅僅在構造函數中用到,若是對象須要不少個這樣的函數,那麼就須要在外部定義不少個這種函數,這無疑會致使代碼很難組織。
函數建立以後都會有一個 prototype
屬性,每一個使用該構造函數建立的對象都有一個 [[prototype]]
內部屬性指向它。
使用原型的好處在於它全部的屬性和方法會在實例間共享,而且這個共享的屬性和方法是直接在原型上設置的。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function() { console.log(this.name); }; const person1 = new Person(); person1.sayName(); // "Nicholas" const person2 = new Person(); person2.sayName(); // "Nicholas" console.log(person1.sayName == person2.sayName); // true
關於原型的工做原理,能夠查看下面三篇文章,看完以後相信你對原型的認識比大多數人都要深入!
對象中屬性的查找機制:
當從對象中訪問一個屬性的時候,JS 引擎將會按屬性名進行查找。JS 引擎會先查找對象自身。若是找到了這個屬性,就會中止查找並返回屬性對應的值,若是在對象自身沒有找到,則會經過原型鏈到原型對象中繼續查找這個屬性,若是找到了這個屬性,就會中止查找並返回屬性對應的值,不然會繼續到上層原型鏈中查找,直到碰到 null
。
當一個屬性添加到實例中時,這個屬性會覆蓋原型上的同名屬性,這個覆蓋指的是查找的時候不會到原型中查找同名屬性。即便屬性的值被賦值爲 null
或 undefined
,它依然會阻止到原型鏈上訪問。因此若是想要訪問,就須要刪除這個屬性,使用 delete obj.xx
。
可使用 hasOwnProperty
判斷實例是否擁有某個屬性,返回 true
則表示實例自己擁有該屬性,不然表示它沒有這個屬性。當一個屬性存在於原型鏈上時,能夠訪問到這個屬性,可是使用 hasOwnProperty
將返回 false
。
in
操做符in
操做符用在兩個地方,一個是用在 for ... in
循環中,另外一個是單獨使用。單獨使用時,返回 true
表示屬性能夠在對象或者其原型鏈上找到。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function() { console.log(this.name); }; const person1 = new Person(); const person2 = new Person(); console.log(person1.hasOwnProperty("name")); // false console.log("name" in person1); // true person1.name = "Greg"; console.log(person1.name); // "Greg" - from instance console.log(person1.hasOwnProperty("name")); // true console.log("name" in person1); // true console.log(person2.name); // "Nicholas" - from prototype console.log(person2.hasOwnProperty("name")); // false console.log("name" in person2); // true delete person1.name; console.log(person1.name); // "Nicholas" - from the prototype console.log(person1.hasOwnProperty("name")); // false console.log("name" in person1); // true
能夠經過組合使用 hasOwnProperty
和 in
來實現判斷一個屬性是否存在於原型鏈上:
function hasPrototypeProperty(object, name) { return !object.hasOwnProperty(name) && name in object; } const obj = Object.create({ name: "lxfriday" }); console.log(obj); console.log(hasPrototypeProperty(obj, "name"));
for ... in
Object.keys()
Object.getOwnPropertyNames/Symbols()
和 Object.assign()
在處理屬性枚舉順序的時候會有很大差異。
for ... in
Object.keys()
沒有肯定的枚舉順序,它們的順序取決於瀏覽器實現。
而 Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
和 Object.assign()
是有肯定的枚舉順序的。
const k2 = Symbol("k2"); const k1 = Symbol("k1"); const o = { 1: 1, [k2]: "sym2", second: "second", 0: 0, first: "first" }; o[k1] = "sym1"; o[3] = 3; o.third = "third"; o[2] = 2; // [ '0', '1', '2', '3', 'second', 'first', 'third' ] console.log(Object.getOwnPropertyNames(o)); // [ Symbol(k2), Symbol(k1) ] console.log(Object.getOwnPropertySymbols(o));
ES 2017 引入了兩個靜態方法來將對象的內容轉換爲可迭代的格式。
Object.values()
返回對象值構成的數組; Object.entries()
返回一個二維數組,數組中的每一個小數組由對象的屬性和值構成,相似於 [[key, value], ...]
。
const o = { foo: "bar", baz: 1, qux: {} }; console.log(Object.values(o)); // ["bar", 1, {}] console.log(Object.entries(o)); // [["foo", "bar"], ["baz", 1], ["qux", {}]]
在輸出的數組中,非字符串的屬性會轉換成字符串,上述的兩個方法對引用類型是採起的淺拷貝。
const o = { qux: {} }; console.log(Object.values(o)[0] === o.qux); // true console.log(Object.entries(o)[0][1] === o.qux); // true
symbol 鍵名會被忽略掉。
const sym = Symbol(); const o = { [sym]: "foo" }; console.log(Object.values(o)); // [] console.log(Object.entries(o)); // []
上面的例子中,給原型賦值都是一個個賦,比較繁瑣,看看下面的賦值方式:
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
上面的例子中,Person
的原型直接指向一個對象字面量,這種方式最終的結果和前面的單個賦值是同樣的,除了原型的 constructor
屬性,constructor
再也不指向 Person
構造函數。默認狀況下,當一個函數建立的時候,會建立一個 prototype
對象,而且這個對象上的 constructor
屬性也會自動指向這個函數。因此這種作法覆蓋了默認的 prototype
對象,意味着 constructor
屬性指向新對象的對應屬性。雖然 instanceof
操做符依然會正常工做,可是已經沒法用 constructor
來判斷實例的類型。看下面的例子:
const friend = new Person(); console.log(friend instanceof Object); // true console.log(friend instanceof Person); // true console.log(friend.constructor == Person); // false console.log(friend.constructor == Object); // true
若是 constructor
屬性很重要,那麼你能夠手動的給它修復這個問題:
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
不過上面的設置方法有一個問題,constructor
的屬性描述以下
{ value: [Function: Person], writable: true, enumerable: true, configurable: true }
咱們再看看 Object.prototype.constructor
:
{ value: [Function: Object], writable: true, enumerable: false, configurable: true }
咱們本身賦值時枚舉屬性會被默認設置爲 true
,因此須要經過 Object.defineProperty
來設置不可枚舉:
Object.defineProperty(Person.prototype, "constructor", { value: Person, enumerable: false, configurable: true, writable: true });
咱們知道原型屬性對全部實例是共享的,當原型屬性是原始值時沒有問題,當原型屬性是引用類型時將會出現問題。看看下面的例子:
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", friends: ["Shelby", "Court"], sayName() { console.log(this.name); } }; const person1 = new Person(); const person2 = new Person(); person1.friends.push("Van"); console.log(person1.friends); // "Shelby,Court,Van" console.log(person2.friends); // "Shelby,Court,Van" console.log(person1.friends === person2.friends); // true
上述例子中,原型屬性 friends
本來是一個包含兩個字符串的數組,可是因爲 person1
修改了它的內容,致使了原型上的這個屬性被更改了,因此 person2
訪問的時候也會打印三個字符串。
因爲這個問題,原型模式並不會單獨使用,咱們常常會結合構造函數和原型來建立對象。
咱們知道,使用構造函數或者原型建立對象都會存在問題,接下來咱們組合使用這二者來解決上面的問題。
爲了解決上面的問題,咱們能夠把全部對象相關的屬性定義在構造函數內,把全部共享屬性和方法定義在原型上。
// 把對象相關的屬性定義在構造函數中 function Human(name, age){ this.name = name, this.age = age, this.friends = ["Jadeja", "Vijay"] } // 把共享屬性和方法定義在原型上 Human.prototype.sayName = function(){ console.log(this.name); } // 使用 Human 構造函數建立兩個對象 var person1 = new Human("Virat", 31); var person2 = new Human("Sachin", 40); // 檢查 person1 和 person2 的 sayName 是否指向了相同的函數 console.log(person1.sayName === person2.sayName) // true // 更改 person1 的 friends 屬性 person1.friends.push("Amit"); // 輸出: "Jadeja, Vijay, Amit" console.log(person1.friends) // 輸出: "Jadeja, Vijay" console.log(person2.friends)
咱們想要每一個實例對象都擁有 name
age
和 friends
屬性,因此咱們使用 this
把這些屬性定義在構造函數內。另外,因爲 sayName
是定義在原型對象上的,因此這個函數會在全部實例間共享。
在上面的例子中,person1
對象更改 friends
屬性時, person2
對象的 friends
屬性沒有更改。這是由於 person1
對象更改的是本身的 friends
屬性,不會影響到 person2
內的。
往期精彩:
關注公衆號能夠看更多哦。
感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。