JS-原型,原型鏈,基於原型和class方式的繼承,一篇文章就夠了!

原型

每個函數(構造函數)都有一個 prototype 屬性,指向該函數(構造函數)的原型對象。實例沒有 prototype 屬性,但是有 __proto__ 屬性。函數同時有 prototype 和 __proto__ 屬性。

function Person(name) {
  this.name = name;
}

let person = new Person('xiaoming');

person.__proto__ === Person.prototype; // true

// 因爲函數也是對象,所以也有 __proto__ 屬性,指向 Function.prototype
Person.__proto__ === Function.prototype //true

由字面量創建的普通對象是Object的實例,
由 function 關鍵字聲明的函數是Function的實例

let obj = {};
obj.__proto__ === Object.prototype; // true

function fn() {};
fn.__proto__ === Function.prototype; // true

__proto__ 屬性雖然在ECMAScript 6語言規範中標準化,但是不推薦被使用,現在更推薦使用Object.getPrototypeOfObject.getPrototypeOf(obj) 也可以獲取到obj對象的原型。

Object.getPrototypeOf(person) === person.__proto__; // true

原型鏈

當訪問一個對象的屬性時,先在對象的本身找,找不到就去對象的原型上找,如果還是找不到,就去對象的原型(原型也是對象,也有它自己的原型)的原型上找,如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回 undefined。

這條由對象及其原型組成的鏈就叫做原型鏈

原型鏈存在的意義就是繼承:訪問對象屬性時,在對象本身找不到,就在原型鏈上一層一層找。說白了就是一個對象可以訪問其他對象的屬性。

繼承存在的意義就是屬性共享:好處有二:一是代碼重用,字面意思;二是可擴展,不同對象可能繼承相同的屬性,也可以定義只屬於自己的屬性

原型鏈的頂端

Object 類型

Object.prototype.__proto__ 是原型鏈的頂端了,指向 null

let obj = {};
obj.__proto__ === Object.prototype; //true
Object.prototype.__proto__ === null; //true

Function 類型

對象都是被構造函數創建的,函數對象的構造函數就是 Function ,注意這裏 F 是大寫。

函數的最大的構造函數爲 Function,其也有 prototype 屬性,由於 Function.prototype 爲一個對象,所以 Function.prototype.__proto__ 指向 Object.prototype

Array.__proto__ === Function.prototype; //true
Object.__proto__ === Function.prototype; // true

同時,由於 Function 也是一個對象,所以其也有 __proto__ 屬性,規定其屬性指向 Function.prototype

function fn() {};
fn.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype; // true
Function.__proto__ === Function.prototype; // true

原型鏈完整圖示

instanceOf 的原理

作用:判斷實例對象與構造函數之間是否爲繼承關係
原理:判斷實例對象的 __proto__ 屬性和構造函數的 prototype 屬性,是否爲同一個引用。
注意點:如果存在多層繼承關係,instanceof 會一直沿着原型鏈往上找。

function Person () {};
let person = new Person();
person instanceof Person; // true
person instanceof Object; // true

person.__proto__ === Person.prototype; // true
Person.prototype.__proto__ === Object.prototype; // true

由上面可以看出,如果用 instanceof 是無法準確的判斷出實例對象是否直接繼承自該構造函數。此時需要使用 constructor。

person.__proto__.constructor === Person; // true
person.__proto__.constructor === Object; // false

小結:

  • Object.prototype.__proto__ === null
  • Array.__proto__ === Function.prototype
  • Object.__proto__ === Function.prototype
  • Function.prototype.__proto__ === Object.prototype
  • Function.__proto__ === Function.prototype
  • instanceof 不能準確的判斷實例是否直接繼承於構造函數

繼承

1.原型鏈法

基本原理是:將父類的實例賦值給子類的原型。

Son.prototype = new Father()

// 父類
function Staff() {
  this.company = 'tianchuang';
  this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
  return this.company;
};

// 子類
function Coder(name, skill) {
  this.name = name;
  this.skill = skill;
}

// 繼承 Staff
Coder.prototype = new Staff();

// 因爲子類原型的指向已經變了,所以需要把子類原型的contructor指向子類本身
Coder.prototype.constructor = Coder;

// 給子類原型添加屬性
Coder.prototype.getInfo = function() {
  return {
    name: this.name,
    skill: this.skill
  };
};

let coder = new Coder('小明', 'javascript');

coder.getInfo(); // {name: '小明', skill: 'javascript'}
coder.getComName(); // 'tianchuang'

這種繼承方式的缺點:

子類的實例可以訪問父類的私有屬性,子類的實例還可以更改該屬性,這樣不安全。

let coder1 = new Coder('zhangsan', 'python');
let coder2 = new Coder('liutian', 'java');
coder1.list; // []
coder1.list.push(1); //[1]
coder2.list // [1]

2. 借用構造函數

原理:在子類構造函數中,使用call來將子類的this綁定到父類中去

// 父類
function Staff() {
  this.company = 'tianchuang';
  this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
  return this.company;
};

// 子類
function Coder(name, skill) {
  Staff.call(this);
  this.name = name;
  this.skill = skill;
}

let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');

coder.getComName(); // Uncaught TypeError: coder.getComName is not a function
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]

優點:

借用構造函數法可以解決原型中引用類型值被修改的問題;

缺點:

只能繼承父對象的實例屬性和方法,不能繼承父對象原型屬性和方法

組合繼承

將原型繼承和借用構造函數兩種方式組合起來

// 父類
function Staff() {
  this.company = 'tianchuang';
  this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
  return this.company;
};

// 子類
function Coder(name, skill) {
  Staff.call(this); // 第一次調用
  this.name = name;
  this.skill = skill;
}

Coder.prototype = new Staff();// 第二次調用

Coder.prototype.constructor = Coder;

let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');

coder.getComName();
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]

優點:

可以保證每個函數有自己的屬性,可以解決原型中引用類型值被修改的問題;
子類的實例可以繼承父類原型上面的屬性和方法

缺點:

在實例化子類的過程中,父類構造函數調用了兩次

寄生組合式繼承(推薦)

所謂寄生繼承:通過 Object.create() 將子類的原型繼承到父類的原型上。

Coder.prototype = Object.create(Staff.prototype);

完整實例:

// 父類
function Staff() {
  this.company = 'tianchuang';
  this.list = [];
}
// 父類的原型
Staff.prototype.getComName = function() {
  return this.company;
};

// 子類
function Coder(name, skill) {
  Staff.call(this);
  this.name = name;
  this.skill = skill;
}

Coder.prototype = Object.create(Staff.prototype);

Coder.prototype.constructor = Coder;

let coder = new Coder('xiaoming', 'java');
let coder2 = new Coder('zhaosan', 'c');

coder.getComName(); // 'tianchuang'
coder.list; //[]
coder.list.push(1); //[1]
coder2.list; //[]

《JavaScript 高級程序設計》中對寄生組合式繼承的誇讚是:

這種方式的高效率體現在紙雕用了一次父類構造函數,並且因此避免了在父類的prototype 上面創建不必要的、多餘的屬性。
同時,原型鏈還能保持不變,可以正常使用 instanceof 和 isPrototypeOf 。
開發人員普遍認爲寄生組合式繼承是引用類型最理想的繼承範式。

class extend 繼承

ES6 中有了的概念,可以通過 class 聲明一個類,通過 extends 關鍵字來實現繼承關係。

class 與 ES5 構造函數的主要區別:

  • class 只能通過 new 來調用,而構造函數則可以直接調用;
  • class 內部所有定義的方法,都是不可枚舉的(non-enumerable)
class Parent {
  constructor(name) {
    this.name = name;
  }
  static say() {
    return 'hello';
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 調用父類的 constructor(name)
    this.age = age;
  }
}

var child1 = new Child('kevin', '18');

console.log(child1);

值得注意的是:

  • super 關鍵字表示父類的構造函數,相當於 ES5 的 Parent.call(this)
子類必須在 constructor 方法中調用 super 方法,否則新建實例時會報錯。這是因爲子類沒有自己的 this 對象,而是繼承父類的 this 對象,然後對其進行加工,如果不調用 super 方法,子類就得不到 this 對象。
正是因爲這個原因,在子類的構造函數中,只有調用 super 之後,纔可以使用 this 關鍵字,否則會報錯。
  • 子類的 __proto__ 屬性指向父類

因此,子類可以繼承父類的靜態方法。

Child.__proto__ === Parent; // true
Child.say(); // hello
  • 子類的原型的 __proto__,總是指向父類的 prototype 屬性
Child.prototype.__proto__ === Parent.prototype; // true

我們會發現,相比寄生組合式繼承,ES6 的 class 多了一個 Object.setPrototypeOf(Child, Parent) 的步驟,即 Child.__proto__ = Parent

Babel 編譯 class extends

ES6 代碼

class Parent {
  constructor(name) {
    this.name = name;
  }
  static say() {
    return 'hello';
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 調用父類的 constructor(name)
    this.age = age;
  }
}

var child1 = new Child('kevin', '18');

Babel 編譯爲:

'use strict';

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

function _inherits(subClass, superClass) {
  // extend 的繼承目標必須是函數或者是 null
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  }

  // 類似於 ES5 的寄生組合式繼承,使用 Object.create 方法,設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性,同時將 子類原型的構造器指向子類本身
  subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });

  // 設置子類的 __proto__ 屬性指向父類
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Parent = function Parent(name) {
  _classCallCheck(this, Parent);

  this.name = name;
};

var Child = function(_Parent) {
  _inherits(Child, _Parent);

  function Child(name, age) {
    _classCallCheck(this, Child);

    // 調用父類的 constructor(name),相當於 Parent.call(this, name)
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

    _this.age = age;
    return _this;
  }

  return Child;
}(Parent);

var child1 = new Child('kevin', '18');

以上代碼,關鍵點在於 _inherits 函數和 _possibleConstructorReturn 函數。

  • _inherits方法使用 Object.create 方法實現了子類原型繼承於父類原型,這就是寄生方式的繼承,建立了Child和Parent的原型鏈關係。

  • _possibleConstructorReturn 方法使用 Parent.call(this) 將子類構造函數(Child)的this綁定到父類構造函數(Parent)中去,通過這樣就將父類的屬性添加到子類的實例對象上去了。

Object.create(superObj) 、Object.setPrototypeOf(subObj, superObject)

Object.create(superObj)

Object.create() 用於從原型對象(superObj)上生成新的實例對象(subObj),其實質是 subObj.__proto__ = superObj,則 subObj 也能夠讀取到 superObj 的屬性。返回值是一個新對象。

let superObj = {name: 'super'};
let subObj = Object.create(superObj);
console.log(subObj.name); // super
console.log(subObj.__proto__ === superObj); // true

Object.create() 其實可以接受兩個參數,第二個參數表示要添加到新創建對象的屬性。注意這裏是給新創建的對象即返回值添加屬性,而不是在新創建對象的原型對象上添加。

const o = Object.create({}, {
  p: {
      value: 42,
      enumerable: false,
      // 該屬性不可寫
      writable: false,
      configurable: true
  }
});
o.p = 24;
console.log(o.p); // 42

Object.create 函數的實現原理:

Object.create = function(obj, props) {
  function F() {};
  F.prototype = obj;
  let newObj = new F();
  Object.assign(newObj, props);
  return newObj;
};

Object.setPrototypeOf(subObj, superObject)

Object.setPrototypeOfObject.create 的作用是一樣的,只不過 Object.setPrototypeOf 是給現有的對象設置原型,返回一個新對象,接受兩個參數。

let subObj = {age: 19};
let superObj = {name: 'tian'};
Object.setPrototypeOf(subObj, superObj);

console.log(subObj.name); // tian
console.log(subObj.__proto__ === superObj); // true

Object.getPrototypeOf(obj)

Object.getPrototypeOf 方法返回一個對象的原型。這是獲取原型對象的標準方法。

let obj = {};
Object.getPrototypeOf(obj); // Object.prototype

function fn() {};
Object.getPrototypeOf(fn); // Function.prototype

參考資料

【前端詞典】繼承(一) - 面試官問的你都會嗎?

深入JavaScript系列(六):原型與原型鏈

ES6 系列之 Babel 是如何編譯 Class 的(上)

ES6 系列之 Babel 是如何編譯 Class 的(下)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章