理解JavaScript的原型鏈

1. 什麼是對象

在JavaScript中,對象是屬性的無序集合,每一個屬性存放一個原始值、對象或函數。javascript

1.1 建立對象

在JavaScript中建立對象的兩種方法:
① 字面上:html

var myObj = {
    key: value,
    // ...
};

② 面向對象:java

var myObj = new Object();
myObj.key = value;

注意:在對象中,屬性名永遠都是字符串。若是你使用string(字面量)之外的其餘值做爲屬性名,那它首先會被轉換爲一個字符串。es6

1.2 內置對象

JavaScript中的內置對象分爲構造器對象和普通對象。
構造器對象:編程

  • Object
  • Boolean
  • String
  • Number
  • Function
  • Array
  • RegExp
  • Date
  • Error

其餘對象:app

  • Math
  • JSON
  • 全局對象

內置對象,其實也叫內置構造器,它們能夠經過new的方式建立一個新的實例對象。函數

2. 原型鏈

先上圖。
this

2.1 __proto__prototypeconstructor

在JavaScript中,每一個對象都擁有一個原型對象prototype,而指向該原型對象的內部指針則是__proto__屬性。
__proto__屬性先後各兩個下劃線,表示它本質是一個內部屬性,而不是一個正式的對外的API。prototype

2.1.1 經過{}建立的對象

var myObj = {};
console.log(myObj.__proto__ === Object.prototype); // true

2.1.2 函數對象

JavaScript中的函數也是對象,它的原型是Function.prototype指針

var Foo = function() {}   
Foo.__proto__ === Function.prototype // true

經過函數構造的實例對象,其原型指針__proto__會指向該函數的prototype屬性。

function Foo(){}
var a = new Foo();   
console.log(a.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
// constructor
console.log(Foo.prototype.constructor === Foo); // true

原型對象都包含一個指向構造函數的指針constructor

注意constructor屬性並非實例對象的屬性,而是構造函數的原型對象的constructor屬性。

console.log(a.constructor === Foo); // true
console.log(a.constructor === Foo.prototype.constructor); // true

console.log(a.hasOwnProperty("constructor")); //false
console.log(Foo.prototype.hasOwnProperty("constructor")); //true

a.constructor經過原型鏈往上找到其原型對象Foo.prototypeconstructor屬性。

2.1.3 內置對象

var a = new Array();   
a.__proto__ === Array.prototype // true

Array.prototype自己也是一個對象,也有繼承的原型:

a.__proto__.__proto__ === Object.prototype  // true   
// 等同於 Array.prototype.__proto__ === Object.prototype

那麼Object的原型指向的是誰呢?

a.__proto__.__proto__.__proto__ === null  // true   
// 等同於 Object.prototype.__proto__ === null

總結:
原型鏈的意義在於,當讀取對象的某個屬性時,JavaScript引擎先尋找對象自己的屬性,若是找不到,就到它的原型去找,若是仍是找不到,就到原型的原型去找。以此類推,若是直到最頂層的Object.prototype仍是找不到,則返回undefined。(全部對象的原型鏈的頂端是Object.prototype

3. ES6的Class(類)

3.1 概述

JavaScript語言的傳統方法是經過構造函數,定義並生成新對象。下面是一個例子。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

// 下面覆蓋了原型鏈上Object.prototype.toString方法
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);
console.log(p.toString()); // (1, 2)

ES6引入了Class這個概念,經過class關鍵字能夠定義類。
ES6的class能夠看做是一種語法糖,它的絕大部分功能,ES5均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。

//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

console.log(typeof Point); // "function"
console.log(Point === Point.prototype.constructor); // true

var p = new Point(1, 2);
console.log(p.toString()); // (1, 2)

上面代碼代表,類的數據類型就是函數,構造函數的prototype屬性,在ES6的"類"上面繼續存在。
事實上,類的全部方法都定義在類的prototype屬性上面。

class Point {
  constructor() {}
  toString() {}
  toValue(){}
}

// 等同於

Point.prototype = {
  toString(){},
  toValue(){}
};

3.2 constructor方法

constructor方法是類的默認方法,經過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,若是沒有顯式定義,一個空的constructor方法會被默認添加。

constructor() {}

constructor方法默認返回實例對象(即this),徹底能夠指定返回另一個對象。

class Foo {
  // constructor() {}
}

console.log(new Foo() instanceof Foo); // true
class Foo {
  constructor() {
    return Object.create(null);
  }
}

console.log(new Foo() instanceof Foo); // false

3.3 實例對象

類必須使用new來調用,從而生成類的實例對象。
實例的屬性除非顯式定義在其自己(即定義在this對象上),不然都是定義在原型上(即定義在class上)。

//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

var point = new Point(2, 3);

console.log(point.hasOwnProperty('x')); // true
console.log(point.hasOwnProperty('y')); // true
console.log(point.hasOwnProperty('toString')); // false
console.log(point.__proto__.hasOwnProperty('toString')); // true

類的全部實例共享一個原型對象。這也意味着,經過實例的__proto__屬性能夠爲原型對象添加方法。

var p1 = new Point(2,3);
var p2 = new Point(3,2);

console.log(p1.__proto__ === p2.__proto__); // true

p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"

上面代碼使用實例的__proto__屬性改寫原型,必須至關謹慎,不推薦使用,由於這會改變Class的原始定義,影響到全部實例。

3.4 繼承

Class之間能夠經過extends關鍵字實現繼承,這比ES5的經過修改原型鏈實現繼承,要清晰和方便不少。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}

上面代碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。
子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工。若是不調用super方法,子類就得不到this對象。

ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this

注意:在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正確
  }
}

3.5 類的__proto__prototype

類同時有__proto__屬性和prototype屬性,同時存在兩條繼承鏈。

  1. 子類的__proto__屬性,表示構造函數的繼承,老是指向父類。
  2. 子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。
class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

這樣的結果是由於,類的繼承是按照下面的模式實現的。

class A {
}

class B {
}

// B的實例繼承A的實例
Object.setPrototypeOf(B.prototype, A.prototype);

// B繼承A的靜態屬性
Object.setPrototypeOf(B, A);

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

這裏只介紹JavaScript的原型鏈相關的內容,若是想了解關於ES6 Class的更多內容請閱讀ES6文檔。
參考:

相關文章
相關標籤/搜索