@javascript
ECMAScript 2017 新增了兩個靜態方法,用於將對象內容轉換爲序列化的——更重要的是可迭代的——格式。java
Object.values() Object.entries()
Object.values()返回對象值的數組,Object.entries()返回鍵/值對的數組。數組
const o = { foo: 'bar', baz: 1, qux: {} }; console.log(Object.values(o)); // ["bar", 1, {}] console.log(Object.entries((o))); // [["foo", "bar"], ["baz", 1], ["qux", {}]]
注意,非字符串屬性會被轉換爲字符串輸出。另外,這兩個方法執行對象的淺複製:app
符號屬性會被忽略函數
爲了減小代碼冗餘,也爲了從視覺上更好地封裝原型功能,直接經過一個包含全部屬性和方法的對象字面量來重寫原型成爲了一種常見的作法。this
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
這樣重寫以後,Person.prototype 的 constructor 屬性就不指向 Person了。上面的寫法徹底重寫了默認的 prototype 對象,所以其 constructor 屬性也指向了徹底不一樣的新對象(Object 構造函數)。雖然 instanceof 操做符還能可靠地返回值,但咱們不能再依靠 constructor 屬性來識別類型了。
若是 constructor 的值很重要,則能夠像下面這樣在重寫原型對象時專門設置一下它的值:prototype
function Person() { } Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
原生 constructor 屬性默認是不可枚舉的。使用 Object.defineProperty()方法來定義constructor 屬性3d
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }; // 恢復 constructor 屬性 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
由於從原型上搜索值的過程是動態的,因此即便實例在修改原型以前已經存在,任什麼時候候對原型對象所作的修改也會在實例上反映出來。就是先建立實例,而後給原型賦值,因爲是指針,因此後賦予的值,在以前創造的對象上也能訪問到。指針
雖然隨時能給原型添加屬性和方法,並可以當即反映在全部對象實例上,但這跟重寫整個原型是兩回事。實例的[[Prototype]]指針是在調用構造函數時自動賦值的,這個指針即便把原型修改成不一樣的對象也不會變。重寫整個原型會切斷最初原型與構造函數的聯繫,但實例引用的仍然是最初的原型。記住,實例只有指向原型的指針,沒有指向構造函數的指針。code
function Person() {} let friend = new Person(); Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } }; friend.sayName(); // 錯誤
下面能夠看引用圖解:
原型模式之因此重要,不只體如今自定義類型上,並且還由於它也是實現全部原生引用類型的模式。全部原生引用類型的構造函數(包括 Object、Array、String 等)都在原型上定義了實例方法。
經過原生對象的原型能夠取得全部默認方法的引用,也能夠給原生類型的實例定義新的方法。能夠像修改自定義對象原型同樣修改原生對象原型,所以隨時能夠添加方法。
原型的最主要問題源自它的共享特性。
咱們知道,原型上的全部屬性是在實例間共享的,這對函數來講比較合適。
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", friends: ["Shelby", "Court"], sayName() { console.log(this.name); } }; let person1 = new Person(); let 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
若是這是有意在多個實例間共享數組,那沒什麼問題。但通常來講,不一樣的實例應該有屬於本身的屬性副本。這就是實際開發中一般不單獨使用原型模式的緣由。
簡單來講就是給一個子類的原型對象用一個父類的實例對象來代替,因此就能夠在子類的原型鏈中訪問到父類的屬性。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承 SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; let instance = new SubType(); console.log(instance.getSuperValue()); // true
原型鏈擴展了前面描述的原型搜索機制。咱們知道,在讀取實例上的屬性時,首先會在實例上搜索這個屬性。若是沒找到,則會繼承搜索實例的原型。在經過原型鏈實現繼承以後,搜索就能夠繼承向上,搜索原型的原型。
實際上,原型鏈中還有一環。默認狀況下,全部引用類型都繼承自 Object,這也是經過原型鏈實現的。任何函數的默認原型都是一個 Object 的實例,這意味着這個實例有一個內部指針指Object.prototype。這也是爲何自定義類型可以繼承包括 toString()、valueOf()在內的全部默認方法的緣由。
第一種方式是使用 instanceof 操做符。
第二種方式是使用 isPrototypeOf()方法。原型鏈中的每一個原型均可以調用這個方法。
子類有時候須要覆蓋父類的方法,或者增長父類沒有的方法。爲此,這些方法必須在原型賦值以後再添加到原型上。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } // 繼承 SuperType SubType.prototype = new SuperType(); // 新方法 SubType.prototype.getSubValue = function () { return this.subproperty; }; // 覆蓋已有的方法 SubType.prototype.getSuperValue = function () { return false; }; let instance = new SubType(); console.log(instance.getSuperValue()); // false
另外一個要理解的重點是,以對象字面量方式建立原型方法會破壞以前的原型鏈,由於這至關於重寫
了原型鏈。
主要問題出如今原型中包含引用值的時候。在使用原型實現繼承時,原型實際上變成了另外一個類型的實例。這意味着原先的實例屬性搖身一變成爲了原型屬性。
原型鏈的第二個問題是,子類型在實例化時不能給父類型的構造函數傳參。事實上,咱們沒法在不影響全部對象實例的狀況下把參數傳進父類的構造函數。
基本思路很簡單:在子類構造函數中調用父類構造函數。由於畢竟函數就是在特定上下文中執行代碼的簡單對象,因此可使用apply()和 call()方法以新建立的對象爲上下文執行構造函數。
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() { // 繼承 SuperType SuperType.call(this); } let instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" let instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green"
相比於使用原型鏈,盜用構造函數的一個優勢就是能夠在子類構造函數中向父類構造函數傳參。
function SuperType(name){ this.name = name; } function SubType() { // 繼承 SuperType 並傳參 SuperType.call(this, "Nicholas"); // 實例屬性 this.age = 29; } let instance = new SubType(); console.log(instance.name); // "Nicholas"; console.log(instance.age); // 29
能夠在調用父類構造函數以後再給子類實例添加額外的屬性。
盜用構造函數的主要缺點,也是使用構造函數模式自定義類型的問題:必須在構造函數中定義方法,所以函數不能重用。此外,子類也不能訪問父類原型上定義的方法,所以全部類型只能使用構造函數模式。因爲存在這些問題,盜用構造函數基本上也不能單獨使用。
基本的思路是使用原型鏈繼承原型上的屬性和方法,而經過盜用構造函數繼承實例屬性。這樣既能夠把方法定義在原型上以實現重用,又可讓每一個實例都有本身的屬性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age){ // 繼承屬性 SuperType.call(this, name); this.age = age; } // 繼承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function() { console.log(this.age); }; let instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" instance1.sayName(); // "Nicholas"; instance1.sayAge(); // 29 let instance2 = new SubType("Greg", 27); console.log(instance2.colors); // "red,blue,green" instance2.sayName(); // "Greg"; instance2.sayAge(); // 27
組合繼承彌補了原型鏈和盜用構造函數的不足,是 JavaScript 中使用最多的繼承模式。並且組合繼承也保留了 instanceof 操做符和 isPrototypeOf()方法識別合成對象的能力。