對象-高程及月影js視頻學習筆記
對象的深拷貝和淺拷貝
ES5 淺拷貝
Object.assign({}, conf)
只能拷貝一級,深層的源改變,目標也會跟着改變。
遞歸 深拷貝
function deepCopy(des, src) {
for (var key in src) {
if(typeof src[key] !== 'object') {
des[key] = src[key];
} else {
des[key] = des[key] || {};
deepCopy(des[key], src[key]);
}
}
return des;
}
創建對象
構造函數模式
new 和 Object.create
首先了解下Object.create的實現方式
Object.create = function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
new操作符會做一下幾個事情:
- 創建一個新對象
- 將構造函數的作用域賦給新對象
- 執行構造函數中的代碼(爲新創建的對象添加屬性)
- 返回新對象
其中被new的函數就叫構造函數。
注意:
function C() {
this.z = 3;
this.func = function() {console.log(1)}
}
var c1 = new C();
var c2 = new C();
c1.func === c2.func // false
執行c1.func === c2.func
會返回false,這是因爲每次實例化的時候都會創造一個新的函數對象,實際上沒有必要這麼做,首先能想到的是,把要創建的函數放到構造函數外面:
function Person(name, age, job){this.sayName = sayName}
function sayName() {console.log(1)}
var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);
p1.sayName === p2.sayName // true
但是放在全局中就是去了封裝的意義,所以引入了原型的模式。
原型模式
每個函數都有一個prototype(原型)的屬性,這個屬性指向一個對象,可以存儲有特定類型的所有實例共享的屬性和方法。
注:箭頭函數不包含這個屬性。。。
此時,下式依舊成立
function Person() {}
Person.prototype.name = 'aaa';
Person.prototype.sayName = ()=>{console.log(2)};
var p1 = new Person(1,2,3);
var p2 = new Person(1,2,3);
p1.sayName === p2.sayName // true
構造函數與原型對象
Person.prototype就是Person的原型對象,默認情況下,每個函數都存在prototype屬性,這個屬性存的對象裏都默認有一個constructor屬性指向該函數(當然還有一個_ proto_繼承自Object)。
Person.prototype.constructor 指向 Person。
// _ proto_:顯示在具體實例上的一個屬性
// prototype:構造函數上的一個屬性
isPrototypeOf
A.prototype.isPrototypeOf(B):A是不是B的原型對象
B instanceof A:B是不是A的實例
判斷某個對象的[[Prototype]](實例,擁有[[Prototype]]屬性的實例對象,通常瀏覽器的實現是_ proto_)是否指向調用isPrototypeOf()這個方法的對象(原型對象Person.prototype)。
讀取某個對象的屬性時,會按照實例對象、_ proto_ 對象、 _ proto_ 的 _ proto_等等順序依次查找,比如上面person的例子,可以找到p1和p2的sayName和name屬性。
但是如果直接給p1.name賦值,則無法改變 _ proto_對象中的name值,會在對象實例本身添加這個屬性,以後讀取p1.name值時,因先從實例找起,所以不會再讀到原型上的值。
Person
name: "bbb"
__proto__:{name:"aaa", sayName: ......}
但是通過修改_ proto_ 的name屬性,就會同時改變p1和p2的原型的name值 以及 Person的原型對象的name值,因爲Person.prototype === p1. _ proto_ ,Person.prototype === p2. _ proto_ 。
使用delete操作符刪掉p1.name,則又能訪問到原型上的name屬性,注意 delete p1.name只能刪掉實例上的屬性,可以通過delete p1.proto.name刪掉原型上的屬性,同樣的操作原型的話,上述三個都會改變。
hasOwnProperty
可以通過這個方法來判斷某屬性是來自實例還是來自原型。
p1.hasOwnProperty('name') // false
in
可以判斷是否對象實例或原型中有該屬性。可以訪問到不可枚舉的屬性,如constructor和_ proto_、prototype。也可以通過in和hasOwnProperty來判斷屬性是否在原型中。
for-in
只能訪問到可枚舉的實例和原型屬性,constructor和_ proto_、prototype等不可枚舉的訪問不到。
Object.keys(obj)
對象上所有可枚舉的實例屬性。返回的是字符串數組 。
Object.getOwnPropertyNames()
對象上所有實例屬性。返回的是字符串數組 。
原型的動態性
一般的,想之前提到的一樣,每次修改原型對象是,能立即在所有對象實例中反應出來,因爲他們之間的連接是一個指針,而給一個副本。
但是,重寫原型對象的話,把原型修改爲另一個對象,就切斷構造函數與最初原型之間的聯繫。
重寫之前的實例中的[[Prototype]]指針仍指向最初的原型。
在重寫原型對象之後創建的實例[[Prototype]]指針指向重寫的原型。z
// 重寫原型對象
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person, // 如果不加這個的話,重寫這裏會沒有constructor,prototype的prototype裏會有Object的constructor。但是這種寫法也有一個問題,constructor會變成可枚舉的,所以也可以向下面那種方式寫。
name: 'aaa'
}
// Object.defineProperty(Person.prototype, 'constructor', {
// enumerable: false,
// value: Person
// });
friend.sayName(); // error
原生對象的原型
原生對象比如Object,Array,String等,比如Array.prototype中有sort()方法。
根據動態性,可以給原生對象的原型上添加方法,比如Array,這樣當前環境的所有數組都可以調用到新添加的方法。
PS: 不推薦在產品化的程序中修改原生對象的原型,這樣可能會導致明明衝突,而且,也可能會意外的重寫原生方法。
如果往數組上添加新方法,因爲默認可枚舉,所以for-in操作數組時,會把往數組上添加的新特性一起for-in出來。
解決這個問題,可以使用Object.defineProperty來給原型對象添加方法。使用此方法時,默認不可枚舉。
原型模式創建對象的優缺點
優點
可以讓所有對象實例共享它所包含的屬性和方法(直接定義在構造函數中的函數,每個實例都會創造一個新的函數)
缺點
- 只用原型模式,沒有構造函數傳遞初始化參數這一環節,會導致默認情況下都取得相同的屬性值。
- 由於原型中的屬性時被所有實例共享的,方法(function)比較適合這種共享的模式,其餘的,改變一個實例的屬性時,如果是值類型的,會在實例上創建這個屬性,覆蓋掉原型中的屬性,不會影響到其他的實例;但是如果是引用類型的,比如下面這個例子,就會導致所有實例的該屬性都改變,因爲此時相當於直接在操作原型,並沒有在實例上添加這個屬性。
function Person() {}
Person.prototype = {
constructor: Person,
name: 'aaa',
friends: [1, 2, 3]
}
var friend1 = new Person();
var friend2 = new Person();
friend1.friends.push(4);
console.log(friend1.friends) // [1, 2, 3, 4]
console.log(friend2.friends) // [1, 2, 3, 4]
組合使用構造函數模式和原型模式
在構造函數中定義各自特有的屬性,在prototype中寫共享的方法。
通常都是用這種方式創建自定義類型。
動態原型模式
在構造函數中初始化原型(僅在必要的情況下)
可以通過檢查某個應該存在的方法是否有效,來決定是否要初始化原型。
function Person(name) {
this.name = name;
// 方法
if(typeof this.name !== "function") {
Person.prototype.sayName = function(){console.log(1)}
}
}
但是只要有一次創建實例時,typeof this.name !== “function”,那麼所有實例(之前創建和之後)都會有sayName這個方法。所以不能使用對象字面量來重寫原型,這樣就會切斷之前創建的實例與新原型之間的聯繫。
寄生構造函數模式
封裝創建對象的代碼,再返回新創建的對象。
構造函數在不返回值的情況下,默認會返回新對象的實例(new操作符做的事情),寄生模式相當於手動做new操作符的一些事情,可以重寫new調用構造函數時返回的值。
function Person(name) {
var o = new Object();
o.name = name;
return o;
}
這種方式有個問題,因爲相當於改變了new的默認行爲,所以不存在默認的原型,原型就是Object,所以不能使用instanceOf來確定對象類型,所以一般情況下不要使用這種模式。
繼承(原型鏈)
繼承包括接口繼承和實現繼承。
實現繼承通過原型鏈實現,利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
…prototype = new Object()的方式才能繼承,讓一個原型對象等於另一個類型的實例。
直接…prototype = …prototype只是一直在修改原型,而不是鏈式繼承。
這種方式有個弊端,在給對象原型賦值的時候,實例化了另一個類型(eg:A.prototype = new B(); ),即調用了類型B的構造函數,通常我們希望在實例化A的時候,再調用B的構造函數,如果B的構造函數有一些方法,或者需要傳參的方法,這種傳undefined參數的實例化可能會引發一些問題,可以用Object.create()來解決:
A.prototype = Object.create(B.prototype);
前文中提到過,Object.create是創建了一個構造函數爲空新對象,賦原型在實例化,所以可以避免可能的構造函數異常執行。
原型鏈的問題
- 之前原型模式創建對象的缺點中提到過,包含引用類性值的原型造成的問題。(不單獨使用原型鏈,放到構造函數中)
- 創建子類型實例時,不能向父類型(子類型繼承父類型)中傳參。(ES6中可以使用
super([arguments])
,比如react中的super(props),還有其他的解決辦法,見下面的借用構造函數)
。
借用構造函數
使用apply()、call()等方法在新創建的對象上執行繼承對象的構造函數。還可以通過此種方式綁定當前this並傳參,或者用super。
getter和setter
使用Object.defineProperty()定義屬性時,可以設置get、set、enumerable。