javascript的原型和繼承

baiya發表於2018-05-05

1.物件的 [[Prototype]]屬性

每個物件都有[[Prototype]]屬性,用於指向物件的原型物件。

當試圖訪問一個物件的屬性時,它不僅僅在該物件上搜尋,還會搜尋該物件的原型,以及該物件的原型的原型,依次層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

遵循ECMAScript標準,someObject.[[Prototype]] 符號是用於指向 someObject的原型。從 ECMAScript 6 開始,[[Prototype]] 可以通過Object.getPrototypeOf()和Object.setPrototypeOf()訪問器來訪問。這個等同於 JavaScript 的非標準但許多瀏覽器實現的屬性__proto__ 。

下面有個例子,手動修改函式的[[Prototype]]屬性來構造一個原型鏈。

    // http://jsbin.com/wovunob/4/edit?js,console
    
    const a = {x: 1};
    const b = {y: 2};
    const c = {z: 3};
    a.__proto__ = b
    b.__proto__ = c
    console.log(a.x) //1
    console.log(a.y) //2
    console.log(a.z) //3

複製程式碼

因此 [[Prototype]]屬性是javascript原型鏈的基礎。

這引出一個問題,可是到底物件的原型又是什麼呢?下面會講到

關於程式碼中直接操作__proto__屬性: 其實官方是建議使用Object.setPrototypeOf 和 Object.getPrototypeOf ,我這裡偷個懶,工作中還是要認真做。

2. 函式的prototype屬性

只有函式才有prototype屬性。

當你建立函式時,JS會為這個函式自動新增prototype屬性,指向一個物件。這個物件含有constructor屬性,你也可以在裡面新增一些方法來實現物件之間的方法函式的共享。

javascript的原型和繼承

而一旦你把這個函式當作建構函式(constructor)呼叫(即通過new關鍵字呼叫),那麼JS就會幫你建立該建構函式的物件例項。我們前面知道物件都是有[[Prototype]]屬性的,該物件的屬性指向自己的原型物件,我們發現原型物件正是建構函式的prototype指向的物件。正因為如此,新生成的物件才可以繼承建構函式的所有屬性和方法。

javascript的原型和繼承

從上面的例子中,我們對物件的原型有個直觀的瞭解。

3.原型物件中的constructor屬性

constructor屬性指向的是物件的建構函式。

javascript的原型和繼承

4.不同生成物件的方法和原型鏈

4.1 語法結構建立的物件

var o = {a: 1};

// o 這個物件繼承了Object.prototype上面的所有屬性
// o 自身沒有名為 hasOwnProperty 的屬性
// hasOwnProperty 是 Object.prototype 的屬性
// 因此 o 繼承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型為 null
// 原型鏈如下:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// 陣列都繼承於 Array.prototype 
// (Array.prototype 中包含 indexOf, forEach等方法)
// 原型鏈如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// 函式都繼承於Function.prototype
// (Function.prototype 中包含 call, bind等方法)
// 原型鏈如下:
// f ---> Function.prototype ---> Object.prototype ---> null
複製程式碼

4.2 構造器建立的物件

在 JavaScript 中,構造器其實就是一個普通的函式。當使用 new 操作符 來作用這個函式時,它就可以被稱為構造方法(建構函式)。

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g是生成的物件,他的自身屬性有'vertices''edges'.
// 在g被例項化時,g.[[Prototype]]指向了Graph.prototype.
複製程式碼

4.3 Object.create 建立的物件

ECMAScript 5 中引入了一個新方法:Object.create()。可以呼叫這個方法來建立一個新物件。新物件的原型就是呼叫 create 方法時傳入的第一個引數

這實際就是進行繼承了物件方法

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因為d沒有繼承Object.prototype

複製程式碼

Object.create的內部實現:

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

複製程式碼

執行程式碼 jsbin.com/wovunob/7/e…

5.類的繼承

繼承是一種在新物件上覆用現有物件的屬性的形式,這有助於避免重複程式碼和重複資料。 js中的繼承其實就是將建構函式的prototype指向一個要繼承的類的物件。

執行程式碼 jsbin.com/wovunob/8/e…

function Person(){}
Person.prototype.dance = function(){}

function Ninja(){}
Ninja.prototype = new Person()

const n = new Ninja();
console.log(n instanceof Ninja) //true
console.log(n instanceof Person) //true
console.log('dance' in n) //true
console.log(n.constructor === Person) //true

複製程式碼

此時Ninja其實已經繼承了Person,但是要還要修改原型物件的constructor屬性 這裡使用Object.defineProperty來改變屬性。 之所以不直接對Ninja.prototype.constructor進行賦值,是因為這麼做會導致constructor屬性被遍歷到。

執行程式碼 jsbin.com/wovunob/10/…

function Person(){}
Person.prototype.dance = function(){}

function Ninja(){}
Ninja.prototype = new Person()
Object.defineProperty(Ninja.prototype , 'constructor' ,{
  enumerable:false,
  writable:true,
  value:Ninja
})

const n = new Ninja();
console.log(n instanceof Ninja) //true
console.log(n instanceof Person) //true
console.log('dance' in n) //true
console.log(n.constructor === Ninja) //true
console.log(Object.keys(Ninja.prototype)) // []

複製程式碼

6.ES6中的class

ES6中引入了新的關鍵字class,它提供了一種更為優雅的建立物件和實現繼承的方式,底層仍然是原型鏈。

1.使用class建立類

使用方法很簡單

class Person{
  constructor(name){
    this.name = name
  }
  dance(){
    return true
  }
}

const p = new Person('lilei');
console.log(p.name) //"lilei"
console.log(p.dance()) //true
複製程式碼

其實class建立的類的型別還是一個函式,仍然具有prototype屬性,物件的[[prototype]]仍然指向這個原型物件,有區別的是constructor變了class,這個暫時無法理解。

javascript的原型和繼承

2.使用class進行繼承

class Ninja extends Person 就可以了

不用再考慮原型或者覆蓋屬性的副作用了。

http://jsbin.com/wovunob/11/edit?js,console

class Person{
  constructor(name){
    this.name = name
  }
  dance(){
    return true
  }
}

class Ninja extends Person{
    constructor(name,weapon){
    super(name)
    this.weapon = weapon
  }
  wieldWeapon(){
    return true
  }
  
}
const p = new Person('lilei');
console.log(p.name) //"lilei"
console.log(p.dance()) //true

const n = new Ninja('Yoshi','sword')
console.log(n instanceof Person) //true
console.log(n instanceof Ninja) //true
console.log(n.name) //"Yoshi"
console.log(n.dance()) //true
console.log(n.wieldWeapon()) //true
console.log(n.constructor === Ninja) //true
console.log(n.__proto__ === Ninja.prototype) //true
console.log(n.__proto__.__proto__ === Person.prototype) //true

複製程式碼

7.其他

7.1 instanceof

7.2 hasOwnProperty

7.3 改變原型物件的副作用

參考文章

developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

github.com/creeperyang/blog/issues/9

javascript忍者祕籍

相關文章