本文原載自 js-professional.lxfriday.xyz/blog/2019/1…,做爲學習筆記總結呈現。前端
{}
對象字面量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
對應的匿名函數,而這個函數對全部實例來說是沒有必要每次都建立的,他們只須要指向同一個函數便可。學習
因此上面的代碼通過改造以後,變成下面這樣:ui
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
複製代碼
上述的作法雖然解決了重複建立匿名函數的問題,可是又引入了新的問題。this
外面的 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,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。