【學習筆記】JavaScript - 從新認識構造函數、原型、原型鏈、繼承

前言

構造函數

什麼是構造函數

構造函數 就是提供一個生成對象的模板並描述對象基本結構的函數。一個構造函數能夠生成多個對象,這些對象都有相同的結構css

構造函數 自己就是一個函數,不過爲了規範通常將其首字母大寫。構造函數普通函數 的區別在於使用 new 生成實例的函數就是構造函數,直接調用的就是普通函數express

生成對象實例時必須使用 new 命令來調用構造函數,因此構造函數更合理的理解應該是 函數的構造調用json

constructor 返回建立實例對象時構造函數的引用,此屬性的值是對函數自己的引用,而不是一個包含函數名稱的字符串數組

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

var p = new Person(18);
p.constructor === Person; // true
p.constructor === Object; // false
複製代碼

那普通函數建立的實例是否是必定沒有 constructor 屬性呢?不必定瀏覽器

// 普通函數
function person(age) {
    this.age = age;
}
var p = person(20); // undefined
p.constructor; // Cannot read property 'constructor' of undefined

// 普通函數
function person(age) {
    return {
        age: age
    }
}
var p = person(20);
p.constructor === Object; // true
複製代碼

Symbol 是不是構造函數

MDN 是這樣介紹的安全

The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()"markdown

Symbol 是基本數據類型,但做爲構造函數來講它並不完整,由於它不支持語法 new Symbol(),所以認爲其不是構造函數,若要生成實例直接使用 Symbol() 便可(來自 MDN),每一個從 Symbol() 返回的值都是惟一的cookie

new Symbol(1); // Symbol is not a constructor 
Symbol(1); // Symbol(1)
複製代碼

雖然是基本數據類型,但 Symbol(1) 實例能夠獲取 constructor 屬性值數據結構

var a = Symbol(1);  // Symbol(1)
console.log(a.constructor); // ƒ Symbol() { [native code] }
複製代碼

這裏的 constructor 屬性實際上是 Symbol 原型上的,即 Symbol.prototype.constructor 返回建立實例原型的函數, 默認爲 Symbol 函數閉包

constructor 值是否只讀

對於引用類型來講 constructor 屬性值是能夠修改的,但對於基本類型來講是隻讀的

引用類型狀況其值可修改這個很好理解,好比原型鏈繼承方案中就須要對 constructor 從新賦值進行修正

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 設置 Bar 的 prototype 屬性爲 Foo 的實例對象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello JS';

Bar.prototype.constructor === Object; // true
var test = new Bar() // 建立 Bar 的一個新實例
console.log(test); 
複製代碼

image.png

// 修正 Bar.prototype.constructor 爲 Bar 自己
Bar.prototype.constructor = Bar;

var test = new Bar() // 建立 Bar 的一個新實例
console.log(test);
複製代碼

image.png

對於基本類型來講是隻讀的,如 1"1"trueSymbolnullundefined 是沒有 constructor 屬性的)

function Type() {};
const types = [1, "1", true, Symbol(1)];

for(let i = 0, len = types.length; i < len; i ++) {
  types[i].constructor = Type;
  types[i] = [types[i].constructor, types[i] instanceof Type, types[i].toString()];
};
console.log(types.join("\n"));
複製代碼

爲何呢?由於建立它們的是隻讀的原生構造函數 native constructors,這個例子也說明了依賴一個對象的 constructor 屬性並不安全

模擬實現 new

new 運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。 ——(來自於MDN)

當代碼 new Foo(...) 執行時,會發生如下事情:

  • 一個繼承自 Foo.prototype 的新對象被建立
  • 使用指定的參數調用構造函數 Foo 並將 this 綁定到新建立的對象上(new Foo 等同於 new Foo(),即沒有指定參數列表,Foo 不帶任何參數調用的狀況)
  • 由構造函數返回的對象就是 new 表達式的結果,若構造函數沒有顯式返回一個對象,則使用步驟 1 建立的對象
// 初版
function createNew() {
  // 建立一個空的對象
  let obj = new Object(); 
  // 得到構造函數,arguments 中去除第一個參數
  const Con = [].shift.call(arguments);
  // 連接到原型
  // 建立一個原型爲構造器的 prototype 的空對象 obj
  // const obj = Object.create(constructor.prototype); 
  obj.__proto__ = Con.prototype;
  // 綁定 this 實現繼承,使用 apply 改變構造函數 this 的指向到新建的對象,這樣 obj 就能夠訪問到構造函數中的屬性
  Con.apply(obj, arguments);
  // 返回對象
  return obj;
};
複製代碼

測試一下

function Car(color) {
    this.color = color;
}
Car.prototype.start = function() {
    console.log(this.color + " car start");
}

var car = createNew(Car, "black");
car.color;
// black

car.start();
// black car start
複製代碼

上面的代碼已經實現了 80%,如今繼續優化。構造函數返回值有以下三種狀況:

  • 返回一個對象

this 失效,實例 person 中只能訪問到返回對象中的屬性

function Person(age, name) {
    this.age = age;
    return {
        name: name
    }
}

const person = new Person(18, "tn");
person.age; // undefined
person.name; // "tn"
複製代碼
  • 沒有 return,即返回 undefined

實例 person 中只能訪問到構造函數中的屬性,和上面徹底相反

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

const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
複製代碼
  • 返回 undefined 之外的基本類型

實例 person 中只能訪問到構造函數中的屬性,和狀況 1 相反,結果至關於沒有返回值

function Person(age, name) {
    this.age = age;
    return "new person";
}

const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
複製代碼

因此須要判斷下返回值是否是一個對象,如果對象則返回該對象,否則返回新建立的 obj 對象,實現代碼以下

// 第二版
function createNew() {
  // 建立一個空的對象
  let obj = new Object();
  // 得到構造函數,arguments 中去除第一個參數
  const Con = [].shift.call(arguments);
  // 連接到原型,obj 能夠訪問到構造函數原型中的屬性
  obj.__proto__ = Con.prototype;
  // 綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
  const ret = Con.apply(obj, arguments);
  // 優先返回構造函數返回的對象
  return ret instanceof Object ? ret : obj;
};
複製代碼

函數對象和普通對象

常常看到一句話說:萬物皆對象,對象就是屬性的集合(對象裏面的一切都是屬性,只有屬性沒有方法,方法也是一種屬性,由於它的屬性表示爲鍵值對的形式)。而在 JavaScript 中,建立對象有幾種方式,如對象字面量 、經過構造函數 new 一個對象、Object.create() image.png

image.png

暫且先無論上面的代碼有什麼意義,至少能看出都是對象且卻存在着差別性

其實在 JavaScript 中能夠將對象分爲函數對象普通對象

  • 函數對象就是 JavaScript 中用函數來模擬的類實現,如 ObjectFunction 就是典型的函數對象

下述代碼中 obj一、obj二、obj三、obj4 都是普通對象,fun一、fun二、fun3 都是 Function 的實例,即函數對象

function fun1() {};
const fun2 = function() {};
const fun3 = new Function('name','console.log(name)');

const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();

console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
console.log(typeof obj4); // object
複製代碼

所以,全部 Function 的實例都是函數對象,其餘均爲普通對象,其中包括 Function 實例的實例

function Foo() {}
// 這個函數是 Function 的實例對象
// function 就是一個語法糖
// 內部調用了 new Function(...)
複製代碼

JavaScript 中萬物皆對象,而對象皆出自構造(構造函數)

個人理解是全部對象都是由 new 操做符後跟函數調用來建立的,字面量表示法只是語法糖(即本質也是 new,功能不變、使用更簡潔),不管是 function Foo() 仍是 let a = { b : 1 }

對於建立一個對象來講,更推薦使用字面量的方式建立,由於使用 new Object() 的方式建立對象須要經過做用域鏈一層層找到 Object,可是使用字面量的方式就沒這個問題

function Foo() {};
// function 就是個語法糖
// 內部等同於 new Function()

let a = { b: 1 };
// 這個字面量內部也是使用了 new Object()
複製代碼

NumberStringBooleanArrayObjectFunctionDateRegExpError 等都是函數,並且是內置的原生構造函數,在運行時會自動出如今執行環境中

構造函數是爲了建立特定類型的對象,這些經過同一構造函數建立的對象有相同原型,共享某些方法。如:全部的數組均可以調用 push 方法,由於它們有相同原型

原型和原型鏈都是來源於對象而服務於對象的概念

原型(prototype)

JavaScript 常被描述爲一種基於原型的語言 (prototype-based language),這個和 Java 等基於類的語言不同

每一個對象擁有一個原型對象,對象以其原型爲模板,從原型繼承方法和屬性,這些屬性和方法定義在對象的構造器函數的 prototype 屬性上,而非對象實例自己

prototype - object that provides shared properties for other objects

在規範裏,prototype 被定義爲:給其它對象實例提供共享屬性的對象。所以 prototype 本身自己也是對象,只是被用以承擔某個職能罷了

只有函數才擁有該屬性,它是 function 對象的一個顯式原型屬性,當聲明一個函數時該屬性就被自動建立了,它定義了構造函數製造出來的對象實例的公共祖先,經過該構造函數產生的對象能夠繼承該原型上的屬性和方法

基本上全部函數都有這個屬性,可是也有一個例外,若用如下方法建立一個函數,可發現這個函數是不具備 prototype 屬性。由於 Function.prototype 是引擎建立出來的函數對象,引擎認爲不須要給這個對象添加 prototype 屬性

let fun = Function.prototype.bind();
複製代碼

prototype 上面添加屬性和方法,每一個構造出來的對象實例均可繼承這些屬性和方法。雖然每一個對象都是獨立的,但它們都有共同的祖先,當訪問這個對象的屬性時,若對象自己沒有該屬性,則會往上找到它的原型,而後在原型上訪問這個屬性

constructor

prototype 有個默認屬性 constructor,指向一個函數,這個函數就是該對象的構造函數

Person.prototype.constructor === Person // true
複製代碼

constructor 是個公有且不可枚舉屬性,一旦改變了函數的 prototype,那新對象就沒有這個屬性(可經過原型鏈取到 constructor

image.png

注意,每一個對象都有其對應的構造函數,自己或者繼承而來。單從constructor 這個屬性來說只有 prototype 對象纔有。每一個函數在建立時 JavaScript 會同時建立一個該函數對應的 prototype 對象

函數建立的對象.__proto__ === 該函數.prototype
函數.prototype.constructor === 該函數自己

故經過函數建立的對象即便本身沒有 constructor 屬性,它也能經過 __proto__ 找到對應的constructor,因此任何對象最終均可以找到其對應的構造函數

其實這個屬性能夠說是一個歷史遺留問題,它有兩個做用

  • 讓實例對象知道是什麼函數構造了它
  • 若想給某些類庫中的構造函數增長一些自定義的方法,就能夠經過 xx.constructor.method 來擴展

__proto__

一、首先須要明確:__proto__constructor 是對象獨有的;prototype 是函數獨有的
二、但在 JavaScript 中,函數也是對象,所以函數也擁有 __proto__constructor 屬性

每一個對象都有該隱式原型屬性,指向了原型(如果構造函數建立的對象,則指向建立該對象的構造函數的原型)

這裏用 __proto__ 獲取對象的原型,__proto__ 是每一個對象實例上都有的屬性,prototype是構造函數的屬性,這兩個並不同,但 __proto__prototype 指向同一個對象

image.png

__proto__ 指向了 [[prototype]](一個對象或 null),因 [[prototype]] 是內部屬性,並不能從外部訪問到,所以有些瀏覽器實現了 __proto__ 來訪問

所以,ECMAScript 規範說 prototype 應當是一個隱式引用:

  • 經過 ES6 新增的 Object.getPrototypeOf(obj) 訪問指定對象的 prototype 對象
  • 經過 Object.setPrototypeOf(obj, anotherObj) 設置指定對象的 prototype 對象
  • 部分瀏覽器實現了 __proto__ ,使得能夠經過 obj.__proto__ 直接訪問原型,經過 obj.__proto__ = anotherObj 直接設置原型
  • ES6 規範只好向事實低頭,將 __proto__ 屬性歸入了規範的一部分,以確保 Web 瀏覽器的兼容性

__proto__ 屬性既不能被 for...in 遍歷出來,也不能被 Object.keys(obj) 查找出來

其實 __proto__ 是個定義在 Object.prototype 上的訪問器屬性,即用 gettersetter 定義的屬性,訪問對象的 obj.__proto__ 屬性,默認走的是 Object.prototype 對象上 __proto__ 屬性的 get/set 方法

Object.defineProperty(Object.prototype,'__proto__',{
  get() {
    console.log('get')
  }
});
({}).__proto__;
console.log((new Object()).__proto__);
// get
// get

const weakMap = new WeakMap();
Object.prototype = {
  get __proto__() {
    return this['[[prototype]]'] === null ? weakMap.get(this) : this['[[prototype]]'];
  },
  set __proto__(newPrototype) {
    if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);

    const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
    if (newPrototype === null || isObject) {
      // 若是以前經過 __proto__ 設置成 null
      // 此時再經過給 __proto__ 賦值的方式修改原型都是徒勞
      // 表現就是 obj.__proto__ = { a: 1 } 就像一個普通屬性 obj.xxx = { a: 1 }
      if (this['[[prototype]]'] === null) {
        weakMap.set(this, newPrototype);
      } else {
        this['[[prototype]]'] = newPrototype;
      }
    }
  },
  // ... 其它屬性如 toString,hasOwnProperty 等
};
複製代碼

__proto__ 屬性在 ES6 時被標準化,以確保 Web 瀏覽器的兼容性,可是不推薦使用,除了標準化的緣由以外還有性能問題,爲了更好的支持,推薦使用 Object.getPrototypeOf()

若一個對象的 __proto__ 屬性被賦值爲 null,這時它的原型確實已經被修改成 null,但想再經過對 __proto__ 賦值的方式設置原型時是無效的,這時 __proto__ 和一個普通屬性沒有區別,只能經過 Reflect.setPrototypeOfObject.setPrototypeOf 才能修改原型。Reflect.setPrototypeOf 之因此能修改原型是由於它是直接修改對象的原型屬性,即內部直接對對象的 [[prototype]] 屬性賦值,而不會經過 __proto__getter

const obj = { name: 'xiaoming' };

obj.__proto__ = null;
console.log(obj.__proto__); // => undefined
console.log(Reflect.getPrototypeOf(obj)); // => null

// 再次賦值爲 null
obj.__proto__ = null;
console.log(obj.__proto__); // => null

obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
// __proto__ 就像一個普通屬性同樣 obj.xxx = { a: 1 }
// 並無將原型設置成功
console.log(Reflect.getPrototypeOf(obj)); // => null

Reflect.setPrototypeOf(obj, { b: 2 });
// __proto__ 被設置爲 null 後,obj 的 __proto__ 屬性和一個普通的屬性沒有區別
console.log(obj.__proto__); // => { a: 1 }
// 使用 Reflect.setPrototypeOf 是能夠設置原型的
console.log(Reflect.getPrototypeOf(obj)); // => { b: 2 }
複製代碼

經過改變一個對象的 [[Prototype]] 屬性來改變和繼承屬性會對性能形成很是嚴重的影響且性能消耗的時間也不是簡單的花費在 obj.__proto__ = ... 語句上,它還會影響到全部繼承自該 [[Prototype]] 的對象,若關心性能就不該該修改一個對象的 [[Prototype]]

若要讀取或修改對象的 [[Prototype]] 屬性,建議使用以下方案,可是此時設置對象的 [[Prototype]] 依舊是一個緩慢的操做,若性能是一個考慮問題,就要避免這種操做

// 獲取
Object.getPrototypeOf();
Reflect.getPrototypeOf();

// 修改
Object.setPrototypeOf();
Reflect.setPrototypeOf();
複製代碼

__proto__ 存在於全部的對象上,是對象所獨有的且指向它的原型對象。它的做用就是當你在訪問一個對象屬性時,若該對象內部不存在這個屬性,則會去它的 __proto__ 屬性所指向的對象(原型對象,原型也是對象也有它本身的原型)上查找,若原型對象依舊不存在這個屬性,則去其原型對象的 __proto__ 屬性所指向的原型對象上去查找...以此類推,直到找到 null,返回 undefined,這個查找的過程也就構成了咱們常說的 原型鏈

由於在 JS 中是沒有類的概念的,爲了實現相似繼承的方式,經過 __proto__ 將對象和原型聯繫起來組成原型鏈,得以讓對象能夠訪問到不屬於本身的屬性

image.png

Object.create()

以前說對象的建立方式主要有兩種,一種是 new 操做符後跟函數調用,另外一種是字面量表示法

第三種就是 ES5 提供的 Object.create() 方法,該方法會建立一個新對象,第一個參數接收一個對象,將會做爲與新建立對象關聯的原型對象,第二個可選參數是屬性描述符(不經常使用,默認是 undefined

日常所看到的空對象其實並非嚴格意義上的空對象,它的原型對象指向Object.prototype,還能夠繼承 hasOwnPropertytoStringvalueOf 等方法

若要建立一個新對象同時繼承另外一個對象的 [[Prototype]] ,推薦使用 Object.create()

若想生成一個不繼承任何屬性的對象,可以使用 Object.create(null)

若想生成一個日常字面量方法生成的對象,須要將其原型對象指向Object.prototype

let obj = Object.create(Object.prototype);
// 等價於
let obj = {};
複製代碼
const obj= Object.create(Object.prototype);
obj.__proto__ === Object.prototype; // true

const obj = Object.create(null);
obj.__proto__ === Object.prototype; // false;
console.log(obj.__proto__); // undefined
複製代碼

簡易模擬 Object.create

function createObj(proto) {
    const F = function() {};
    F.prototype = proto;
    return new F();
}
複製代碼

原型鏈

定義

當在一個對象 obj 上訪問某個屬性時,若該屬性不存在於 obj 上,則會經過 __proto__ 去對象的原型即 obj.__proto__ 上去找這個屬性,如有則返回該屬性,沒有則繼續去對象 obj 的原型的原型即 obj.__proto__.__proto__ 去找 ... 重複以上步驟,一直訪問到 純對象的原型Object.prototype,沒有的話繼續往上找即 Object.prototype.__proto__,即 null,此時直接返回 undefined

console.log(new Object().__proto__.__proto__); // null
Object.prototype.__proto__ === null; // null
複製代碼

這就推出了原型鏈之因此叫原型鏈而不叫原型環,說明它是善始善終的,原型鏈的頂層就是 null,返回 undefined,因此原型鏈不會無限的找下去

所以原型鏈能夠描述爲由對象的 __proto__ 屬性將對象和原型聯繫起來直到Object.prototype.__proto__null 的鏈就是原型鏈

function Student(name, grade) {
  this.name = name;
  this.grade = grade;
}

const stu = new Student();
console.log(stu.gender); // => undefined
複製代碼

訪問 stu.gender 的整個過程以下圖: image.png

函數 Student 的原型鏈應該是這樣的

image.png

上文介紹了 prototype__proto__ 的區別,其中原型對象 prototype 是構造函數的屬性,__proto__ 是每一個實例對象上都有的屬性,這兩個並不同,但指向同個對象,如上面例子 stu.__proto__Student.prototype 指向同個對象

那原型鏈的構建是依賴於 prototype 仍是 __proto__ 呢?

上圖中,Student.prototype 中的 prototype 並無構建成一條原型鏈,其只是指向原型鏈中的某一處。原型鏈的構建依賴於 __proto__,如上圖經過 stu.__proto__ 指向 Student.prototypestu.__proto__.__proto__ 指向 Object.prototyp,如此一層一層最終連接到 null

能夠這麼理解,Student 是一個 constructor 也是一個 function,它身上有着 prototype 的 reference,只要調用 stu = new Student() 就會將 stu.__proto__ 指向到 Student 的 prototype 對象

不要使用相似 Bar.prototype = Foo,由於這不會執行 Foo 的原型,而是指向函數 Foo。所以原型鏈將會回溯到 Function.prototype 而不是 Foo.prototype,所以 Foo 原型上的方法將不會在 Bar 的原型鏈上

function Foo() {
  return 'foo';
}
Foo.prototype.getMethod = function() { 
  return 'method';
}
function Bar() {
  return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函數 Foo
const bar = new Bar();
console.dir(bar);
bar.method(); // bar.getMethod is not a function

bar.__proto__ === Foo.prototype; // false
複製代碼

原型鏈上屬性的增刪改查

經過一個對象改變了原型上的引用值類型的屬性,則全部對象實例的這個屬性值都會隨之更改 image.png

依據當自身沒有這個屬性時就會向上往原型查詢的說法,再次刪除這個屬性是否是就能夠刪除原型上的屬性了?然而事實並無,因而可知對象實例並不能刪除原型上的屬性

image.png

誰調用這個方法,這個方法中的 this 就指向這個調用它的對象

image.png

instanceof 操做符

日常判斷一個變量的類型常常會使用 typeof 運算符,但對於引用類型來講並不能很好區分(除了函數對象會返回 function 外其餘都返回 object

來看一下 MDN 上對於 instanceof 運算符的描述

instanceof 運算符用於測試構造函數的 prototype 屬性是否出如今對象實例的原型鏈中的任何位置

instanceoftypeof 很是的相似。instanceof 用於判斷對象是不是某個構造函數的實例,若 obj instanceof A,就說明 objA 的實例 f

它的原理一句話歸納就是:obj instanceof 構造器 A 等同於 判斷 A 的 prototype 是否是 obj 的原型

instanceof 操做符左邊是一個對象,右邊是一個構造函數,在左邊對象的原型鏈上查找(經過 __proto__)直到找到右邊構造函數的 prototype 屬性就返回 true,或查找到頂層 nullObject.prototype.__proto__),就返回 false

// 定義構造函數
function C(){} 
function D(){} 

const o1 = new C();
o1 instanceof C; // true,由於 Object.getPrototypeOf(o1) === C.prototype

o1 instanceof D; // false,由於 D.prototype 不在 o1 的原型鏈上

o1 instanceof Object; // true,由於 Object.prototype.isPrototypeOf(o1) 返回 true
C.prototype instanceof Object; // true,同上

C.prototype = {};
const o2 = new C();

o2 instanceof C; // true

o1 instanceof C; // false,C.prototype 指向了一個空對象,這個空對象不在 o1 的原型鏈上

D.prototype = new C(); // 繼承
const o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 由於 C.prototype 如今在 o3 的原型鏈上
複製代碼

typeof 方法不一樣的是,instanceof 方法要求開發者明確地確認對象爲某特定類型

簡單模擬實現

// 第一種
// 參數 obj 表示 instanceof 左邊的對象
// 參數 Constructor 表示 instanceof 右邊的構造函數
function myInstanceOf(obj, Constructor) {
  // 取構造函數顯示原型
  let rightP = Constructor.prototype; 
  // 取對象隱式原型
  let leftP = obj.__proto__; 
  // 到達原型鏈頂層還未找到則返回 false
  if (leftP === null) {
      return false;
  }
  // 對象實例的隱式原型等於構造函數顯示原型則返回 true
  if (leftP === rightP) {
      return true;
  }
  // 遞歸查找原型鏈上一層
  return myInstanceOf(obj.__proto__, Constructor)
}

// 第二種
function myInstanceof(left, right) {
  let prototype = right.prototype;
  left = left.__proto__;
  while (true) {
    if (left === null) return false;
    if (prototype === left) return true;
    left = left.__proto__;
  }
}
複製代碼

如今就能夠解釋一些比較使人費解的結果了

let fn = function() {};
let arr = [];
fn instanceof Function; // true
fn instanceof Object; // true
// 1. fn.__proto__ === Function.prototype;
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;

arr instanceof Array; // true
arr instanceof Object; // true
// 1. arr.__proto__ === Array.prototype;
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype;

Object instanceof Object; // true
// 1. Object.__proto__ === Function.prototype;
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;

Function instanceof Function; // true
Function instanceof Object; // true
// 1. Function.__proto__ === Function.prototype;
// 2. Function.__proto__.__proto__ === Object.prototype;

Foo instanceof Function; // true 
Foo instanceof Foo; // false
// 1. Foo.__proto__ === Function.prototype;
// 2. Foo.__proto__.__proto__ === Function.prototype.__proto__ = Object.prototype;
// 3. Foo.__proto__.__proto__.__proto__ === Object.prototype.__proto__ = null;
// 4. null
複製代碼

總結:instanceof 運算符用於檢查右邊構造函數的 prototype 屬性是否出如今左邊對象的原型鏈中的任何位置,其實它表示的是一種原型鏈繼承的關係

Object & Function

上面提到的 Object.__proto__ === Function.prototypeFunction.__proto__ === Function.prototype 究竟是爲何呢?

Object.prototype

ECMAScript 上的定義

The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is "Object", and the initial value of the [[Extensible]] internal property is true

Object.prototype 表示 Object 的原型對象,其 [[Prototype]] 屬性是 null,訪問器屬性 __proto__ 暴露了一個對象的內部 [[Prototype]]Object.prototype 並非經過 Object 函數建立的,爲何呢?看以下代碼

function Foo() {
  this.value = 'foo';
}
let foo = new Foo();
foo.__proto__ === Foo.prototype; // true
複製代碼

實例對象的 __proto__ 指向構造函數的 prototype,即 foo.__proto__ 指向 Foo.prototype,但 Object.prototype.__proto__null,因此 Object.prototype 並非經過 Object 函數建立的,那它如何生成的?其實 Object.prototype 是引擎根據 ECMAScript 規範創造的一個對象

因此能夠說:全部實例都是對象,可是對象不必定都是實例

不考慮 null 的狀況下,Object.prototype 就是原型鏈的頂端,全部對象實例均可以繼承它的 toString 等方法和屬性

Function.prototype

ECMAScript 上的定義

The Function prototype object is itself a Function object (its [[Class]] is "Function").

The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object.

The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object

Function.prototype 對象是一個函數(對象),其 [[Prototype]] 內部屬性值指向內建對象 Object.prototypeFunction.prototype 對象自身沒有 valueOf 屬性,其從 Object.prototype 對象繼承了 valueOf 屬性

Function.prototype[[Class]] 屬性是 Function,因此這是一個函數,但又不大同樣。爲何這麼說呢?由於只有函數纔有 prototype 屬性,但並非全部函數都有這個屬性,由於 Function.prototype 這個函數就沒有

Function.prototype
// ƒ () { [native code] }

Function.prototype.prototype
// undefined
複製代碼

下面這個函數也沒有 prototype 屬性

let fun = Function.prototype.bind();
// ƒ () { [native code] }

fun.prototype
// undefined
複製代碼

爲何沒有呢?個人理解是 Function.prototype 是引擎建立出來的函數,引擎認爲不須要給這個函數對象添加 prototype 屬性,否則 Function.prototype.prototype… 將無休無止而且沒有存在的意義

Function.prototype 不可寫、不可配置、不可遍歷,即它永遠指向固定的一個對象且是其餘全部函數的原型對象,全部函數自己的 __proto__ 指向它

引擎首先建立了 Object.prototype ,而後建立了 Function.prototype 而且經過 __proto__ 將二者聯繫了起來

Object

JS 中 ObejctFunction 都是構造函數(構造函數也是函數),和 objectfunction 不是一個東西,分別用於建立 對象函數 實例

ECMAScript 上的定義

The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object

Object 做爲構造函數時,其 [[Prototype]] 內部屬性值指向 Function.prototype,即

Object.__proto__ === Function.prototype; // true
複製代碼

Object 的全貌是:function Object() { ... },它是普通對象的構造函數,當 var foo = {} 時至關於實例化 Object,即 new Object()

使用 new Object() 建立新對象時,這個新對象的 [[Prototype]] 內部屬性指向構造函數的 prototype 屬性,對應就是 Object.prototype

固然也能夠經過對象字面量等方式建立對象

  • 使用對象字面量建立的對象,其 [[Prototype]] 值是 Object.prototype
  • 使用數組字面量建立的對象,其 [[Prototype]] 值是 Array.prototype
  • 使用 function f(){} 函數建立的對象,其 [[Prototype]] 值是 Function.prototype
  • 使用 new fun() 建立的對象,其中 fun 是由 JavaScript 提供的內建構造器函數之一(Object, Function, Array, Boolean, Date, Number, String 等),其 [[Prototype]] 值是 fun.prototype
  • 使用其餘 JavaScript 構造器函數建立的對象,其 [[Prototype]] 值就是該構造器函數的 prototype 屬性
// 原型鏈:o.__proto__ -> Object.prototype -> null
let o = {a: 1};

// 原型鏈:a -> Array.prototype -> Object.prototype -> null
let a = ["yo", "whadup", "?"];

// 原型鏈:f -> Function.prototype -> Object.prototype -> null
function f(){
  return 1;
}

// 原型鏈:fun -> Function.prototype -> Object.prototype -> null
let fun = new Function();

// 原型鏈:foo -> Foo.prototype -> Object.prototype -> null
function Foo() {}
let foo = new Foo();

// 原型鏈:foo -> Object.prototype -> null
function Foo() {
  return {};
}
let foo = new Foo();
複製代碼

Function

ECMAScript 上的定義

The Function constructor is itself a Function object and its [[Class]] is "Function". The value of the [[Prototype]] internal property of the Function constructor is the standard built-in Function prototype object

Function 構造函數是一個函數對象,其 [[Class]] 屬性是 FunctionFunction[[Prototype]] 屬性指向了 Function.prototype,即

Function.__proto__ === Function.prototype; // true
複製代碼

Function 的全貌是:function Function() { ... },它是函數對象的構造函數,當 function foo() {} 時至關於實例化 Function,即 new Function()

咱們知道函數的本質是經過 new Function() 生成的,但Function.prototype 是引擎本身建立的,因此又能夠得出一個結論

不是全部函數都是 new Function() 產生的

Function & Object 雞蛋問題

先看下面代碼

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

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

Object instanceof Object; // true
Object.__proto__.__proto__ === Object.prototype; // true

Function instanceof Function; // true
Function.__proto__ === Function.prototype; // true
複製代碼

Object 構造函數繼承了 Function.prototype,一切函數對象都直接繼承自 Function 對象(系統內置的構造函數),函數對象 包括了 FunctionObjectArrayStringNumberRegExpDate 等,Function 其實不只用於構造函數,它也充當了 函數對象 的構造器

同時 Function 構造函數繼承了 Object.prototype,這裏就產生了 雞和蛋 的問題。由於 Function.prototypeFunction.__proto__ 都指向 Function.prototype

對於 Function.__proto__ === Function.prototype 這一現象有 2 種解釋,爭論點在於 Function 對象是否是由 Function 構造函數建立的一個實例?

  • YES:按照 JavaScript 中實例的定義,ab 的實例即 a instanceof btrue,默認判斷條件就是 b.prototypea 的原型鏈上。而 Function instanceof Functiontrue,本質上即 Object.getPrototypeOf(Function) === Function.prototype,正符合此定義
  • NOFunctionbuilt-in 的對象,即並不存在 Function 對象由 Function 構造函數建立 這樣顯然會形成雞生蛋蛋生雞的問題。實際上當直接寫一個函數時(如 function f() {}x => x),也不存在調用 Function 構造器,只有在顯式調用 Function 構造器時(如 new Function('x', 'return x'))纔有

我的偏向於第二種解釋,即先有 Function.prototype 而後有 function Function(),因此就不存在雞生蛋蛋生雞問題了,把 Function.__proto__ 指向 Function.prototype,我的的理解是:其餘全部的構造函數均可以經過原型鏈找到 Function.prototype,且 Function 本質也是一個函數對象,事實上 Function 只是一個祖先、一個構造函數,並非一個實例出來的函數對象,因此原本不必擁有 __proto__ 這個屬性,但這樣的話會顯得 Function 很另類,因而也給它加上屬性 __proto__ 並指向 Function.prototype ,只是爲了代表 Function 做爲一個原生構造函數,自己也是一個函數對象,並且這也保證了原型鏈的完整,讓 Function 能夠獲取定義在 Object.prototype 上的方法

一切函數對象(包括 Object 對象) 都直接繼承自 Function 對象,Function 對象直接繼承本身,最終繼承自 Object 對象,Object 和 Function 是互相繼承的關係

有了 Function.prototype 之後纔有了 function Function(),而後其餘的構造函數都是 function Function() 生成的

一切對象都繼承自 Object.prototype,而一切函數對象都繼承自 Function.prototype (Function.prototype 最終繼承自 Object.prototype),即普通對象和函數對象的區別是:普通對象直接繼承了 Object.prototype,而函數對象在中間還繼承了 Function.prototype

所以能夠得出如下總結:

  1. Object 是全部對象的爸爸,全部對象均可以經過 __proto__ 找到它
  2. Function 是全部函數的爸爸,全部函數均可以經過 __proto__ 找到它
  3. 全部經過字面量表示法建立的普通對象的構造函數爲 Object
  4. 全部原型對象都是普通對象,構造函數爲 Object
  5. 全部函數的構造函數是 Function
  6. Function.prototypeObject.prototype 沒有原型對象
  7. Function.prototypeObject.prototype 是兩個由引擎建立出來的特殊對象,除了這兩個特殊對象,其餘對象都是經過構造器 new 出來的
  8. 函數的 prototype 是一個對象,即原型。對象的 __proto__ 指向原型,__proto__ 將對象和原型鏈接起來組成了原型鏈
  9. Function.prototype.__proto__ === Object.prototype
  10. Object.__proto__ === Function.prototype
  11. Function.__proto__ === Function.prototype
  12. Function.__proto__ === Object.__proto__
  13. Object.prototype.__proto__ === null
  14. Object => Function.prototype => Object.prototype => null
  15. Function => Function.prototype => Object.prototype => null
  16. 如果自定義的構造函數,造成的原型鏈以下:Foo => Function.prototype => Object.prototype => null
  17. 經過自定義構造函數實例化的對象,造成的原型鏈以下:obj => Foo.prototype => Object.prototype => null

image.png

image.png

內置類型構建過程

JavaScript 內置類型是瀏覽器內核自帶的,瀏覽器底層對 JavaScript 的實現基於 C/C++,那麼瀏覽器在初始化 JavaScript 環境時都發生了什麼?

  • 用 C/C++ 構造內部數據結構建立一個 OP 即 (Object.prototype) 以及初始化其內部屬性但不包括行爲
  • 用 C/C++ 構造內部數據結構建立一個 FP 即 (Function.prototype) 以及初始化其內部屬性但不包括行爲
  • 將 FP 的 [[Prototype]] 指向 OP
  • 用 C/C++ 構造內部數據結構建立各類內置引用類型
  • 將各內置引用類型的[[Prototype]]指向 FP
  • 將 Function 的 prototype 指向 FP
  • 將 Object 的 prototype 指向 OP
  • 用 Function 實例化出 OP、FP,以及 Object 的行爲並掛載
  • 用 Object 實例化出除 Object 以及 Function 的其餘內置引用類型的 prototype 屬性對象
  • 用 Function 實例化出除 Object 以及 Function 的其餘內置引用類型的 prototype 屬性對象的行爲並掛載
  • 實例化內置對象 Math 以及 Grobal

至此全部內置類型構建完成

原型污染

曾經 lodash 爆出了一個嚴重的安全漏洞:Lodash 庫爆出嚴重安全漏洞,波及 400 萬+項目,這個安全漏洞就是因爲原型污染致使的,Lodash 庫中的函數「defaultsDeep」頗有可能會被欺騙添加或修改 Object.prototype 的屬性,最終可能致使 Web 應用程序崩潰或改變其行爲,具體取決於受影響的用例

雖說任何一個原型被污染了都有可能致使問題,但通常提原型污染說的就是 Object.prototype 被污染

原型污染的危害

  • 性能問題:原型被污染會增長遍歷的次數,每次訪問對象自身不存在的屬性時也要訪問下原型上被污染的屬性
  • 致使意外的邏輯 bug:看下面這個從別的大佬的文章中看到的例子
'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');

const isObject = (obj) => obj && obj.constructor && obj.constructor === Object;

function merge(a, b) {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a;
}

function clone(a) {
  return merge({}, a);
}

// Constants
const PORT = 8080;
const HOST = '127.0.0.1';
const admin = {};

// App
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());

app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
  var body = JSON.parse(JSON.stringify(req.body));
  var copybody = clone(body);
  if (copybody.name) {
    res.cookie('name', copybody.name).json({
      done: 'cookie set',
    });
  } else {
    res.json({
      error: 'cookie not set',
    });
  }
});
app.get('/getFlag', (req, res) => {
  var аdmin = JSON.parse(JSON.stringify(req.cookies));
  if (admin.аdmin == 1) {
    res.send('hackim19{}');
  } else {
    res.send('You are not authorized');
  }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
複製代碼

這段代碼的漏洞就在於 merge 函數上,能夠這樣攻擊:

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://127.0.0.1:4000/signup';

curl -vv 'http://127.0.0.1/getFlag'
複製代碼

首先請求 /signup 接口,在 NodeJS 服務中,咱們調用了有漏洞的 merge 方法,並經過 __proto__Object.prototype(由於 {}.__proto__ === Object.prototype) 添加上一個新的屬性 admin 且值爲 1

再次請求 getFlag 接口訪問了 Object 原型上的 admin,條件語句 admin.аdmin == 1true,服務被攻擊

預防原型污染

其實原型污染大多發生在調用會修改或擴展對象屬性的函數時,如 lodash 的 defaults、jQuery 的 extend,預防原型污染最主要仍是要有防患意識,養成良好的編碼習慣

Object.create(null)

  • Object.create(null) 建立沒有原型的對象,即使對它設置 __proto__ 也沒有用,由於它的原型一開始就是 null,沒有 __proro__setter

image.png

Object.freeze(obj) 能夠經過 Object.freeze(obj) 凍結對象 obj,被凍結的對象不能被修改屬性,成爲不可擴展對象。不能修改不可擴展對象的原型,不然會拋 TypeError:

const obj = Object.freeze({ name: 'xiaoHong' });
obj.xxx = 666;
console.log(obj); // => { name: 'xiaoHong' }
console.log(Object.isExtensible(obj)); // => false
obj.__proto__ = null; // => TypeError: #<Object> is not extensible
複製代碼

關於原型污染可閱讀:最新:Lodash 嚴重安全漏洞背後你不得不知道的 JavaScript 知識

繼承

原型存在的意義就是組成原型鏈:引用類型皆對象,每一個對象都有原型,原型也是對象,也有它本身的原型,一層一層的組成了原型鏈
原型鏈存在的意義就是繼承:訪問對象屬性時,在對象自己找不到,就在原型鏈上一層一層往上找,說白了就是一個對象能夠訪問其餘對象的屬性
繼承存在的意義就是屬性共享:好處一是代碼重用(字面意思);好處二是可擴展,不一樣對象可能繼承相同的屬性,也能夠定義只屬於本身的屬性

ES5 繼承實現方式

原型鏈繼承

將父類的實例做爲子類的原型

function Parent() {
     this.name = 'tn';
}
Parent.prototype.getName = function () {
    console.log(this.name);
}
function Son () {};
// 關鍵,建立 Parent 的實例並將該實例賦值給 Son.prototype
Son.prototype = new Parent();
const son1 = new Son();
console.log(son1.getName());  // tn 

// 缺點
function Parent () {
    this.names = ['licy', 'tn'];
}
function Son () {}
Son.prototype = new Parent();
const son1 = new Son();
son1.names.push('yayu');
console.log(son1.names); // ["licy", "tn", "yayu"]

const son2 = new Son();
console.log(son2.names); // ["licy", "tn", "yayu"]
複製代碼

image.png

  • 優勢:父類方法能夠複用,父類的屬性與方法子類都能訪問
  • 缺點:
    • 父類的引用屬性會被全部子類實例共享,子類會繼承過多沒有用的屬性,形成大量的浪費且多個實例對引用類型的操做會被篡改
    • 因爲子類實現的繼承是靠其原型 prototype 對父類進行實例化實現的,所以在構建子類實例時是沒法向父類傳遞參數的,於是在實例化父類時也沒法對父類構造函數內的屬性進行初始化

借用構造函數繼承

使用父類的構造函數來加強子類實例,利用 callapply 可改變 this 指向的特色,將父類構造函數內容複製給子類構造函數,因爲父類中給 this 綁定屬性,所以子類天然也就繼承父類的共有屬性,這是全部繼承中惟一不涉及到 prototype 的繼承

function Parent (name) {
    this.books = ['js','css'];
    this.name = name;
}
Parent.prototype.showBooks = function() {
  console.log(this.books);
}

function Son (name) {
    Parent.call(this, name);
}
const son1 = new Son('tn');
console.log(son1.name); // tn
son1.showBooks(); // TypeError: son1.showBooks is not a function

const son2 = new Son('licy');
console.log(son2.name); // licy

function SuperType(){ 
   this.color=["red","green","blue"]; 
}
function SubType(){ 
   //繼承自 SuperType 
   SuperType.call(this);
}
const instance1 = new SubType(); 
instance1.color.push("black"); 
console.log(instance1.color); //["red", "green", "blue", "black"] 

const instance2 = new SubType(); 
console.log(instance2.color); //["red", "green", "blue"]
複製代碼
  • 優勢:
    • 父類的引用屬性不會被共享,避免了引用類型的屬性被全部實例共享且避免了多個實例對引用類型的操做會被篡改的問題
    • 子類構建實例時能夠向父類傳遞參數
  • 缺點
    • 不能繼承父類原型上的屬性/方法(若原型上的屬性/方法想被子類繼承,就必須放到構造函數中)
    • 父類的方法和屬性不能複用,子類實例的方法每次都是單首創建的,這樣就違背了代碼複用的原則
    • 每一個子類都有父類實例函數的副本,影響性能

組合繼承

原型鏈繼承借用構造函數繼承 結合

用原型鏈實現對原型屬性和方法的繼承,用借用構造函數技術來實現實例屬性的繼承

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"];
}    
SuperType.prototype.sayName = function(){ console.log(this.name); }; 
function SubType(name, age){ 
    // 繼承屬性
    // 第二次調用 SuperType() 
    // 第二次又給子類的構造函數添加了父類的 name, colors 屬性
    // 使用子類建立的實例對象上的同名屬性覆蓋了子類原型中的同名屬性,這形成了性能浪費
    SuperType.call(this, name); 
    this.age = age; 
} 
// 繼承方法,構建原型鏈 
// 第一次調用 SuperType() 
// 第一次給子類的原型添加了父類的 name, colors 屬性
SubType.prototype = new SuperType(); 

// 重寫 SubType.prototype 的 constructor 屬性,指向本身的構造函數 SubType 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){ alert(this.age); }; 

const instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //"Nicholas"; 
instance1.sayAge(); //29 

const instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //"Greg"; 
instance2.sayAge(); //27
複製代碼
  • 優勢:
    • 父類的方法能夠被複用
    • 父類的引用屬性不會被共享
    • 子類構建實例時能夠向父類傳遞參數
    • 能夠繼承父類的屬性和方法,同時也能夠繼承原型的屬性和方法
  • 缺點:
    • 使用子類建立實例對象時,父類調用了兩次,所以產生了兩份實例,其原型中會存在兩份相同的屬性/方法

原型式繼承

ES5 Object.create 的模擬實現,利用一個空對象做爲中介,將傳入的對象做爲該空對象構造函數的原型,object() 對傳入的對象執行了一次淺複製

function object(obj){
  // 聲明一個過渡對象 
  function F(){};
  // 過渡對象的原型繼承傳入的對象
  F.prototype = obj;
  // 返回過渡對象的實例
  return new F();
}

const person = { 
  name: "Nicholas", 
  friends: ["Shelby", "Court", "Van"] 
};

const anotherPerson = object(person); 
const yetAnotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

console.log(anotherPerson.name) // "Greg"
console.log(yetAnotherPerson.name) // "Nicholas"
console.log(yetAnotherPerson.friends) //["Shelby", "Court", "Van", "Rob"]
複製代碼
  • 缺點:
    • 父類的引用類型的屬性值會被全部子類實例共享,改動一個會影響另外一個,這點跟原型鏈繼承同樣
    • 子類構建實例時不能向父類傳遞參數
  • ES5 中 Object.create() 的方法可以代替上面的 object方法,Object.create() 方法規範化了原型式繼承

寄生式繼承

  • 在原型式繼承的基礎上,建立一個僅用於封裝繼承過程的函數,該函數在內部以某種形式來加強對象,最後返回對象
  • 這樣新建立的對象不只僅有父類的屬性和方法,還可新增了別的屬性和方法
function createAnother(original){ 
   const clone = object(original); // 經過調用 object() 函數建立一個新對象 
   // 或
   const clone = Object.create(o);
   clone.sayHi = function(){ 
       // 以某種方式來加強對象 
       alert("hi"); 
   }
   return clone; // 返回這個對象 
}   
const person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
複製代碼
  • 缺點(同原型式繼承)

寄生組合式繼承

寄生組合式繼承是寄生式繼承和借用構造函數繼承的組合,只調用了一次父類構造函數,解決了組合繼承有會兩次調用父類的構造函數形成浪費的缺點

function object(o) {
    //聲明一個過渡對象
    function F() {}
    //過渡對象的原型繼承父對象
    F.prototype = o;
    //返回過渡對象的實例,該對象的原型繼承了父對象
    return new F();
}
function prototype(child, parent) {
    // 複製一份父類的原型副本到變量中
    const prototype = object(parent.prototype);
    // 加強對象,修正由於重寫子類的原型致使子類的 `constructor` 屬性被修改
    prototype.constructor = child;
     // 設置子類原型
    child.prototype = prototype;
}
// 使用時
prototype(Child, Parent);
複製代碼

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

這種方式的高效率體現它只調用了一次 Parent 構造函數而且所以避免了在 Parent.prototype 上面建立沒必要要的、多餘的屬性。與此同時原型鏈還能保持不變,所以可以正常使用 instanceofisPrototypeOf。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式,也是如今不少庫實現的方法

封裝

function inherit (Target, Origin) {
    // 聲明一個過渡對象
    function F () {};
    // 過渡對象的原型繼承父對象,建立了父類原型的淺複製
    F.prototype = Origin.prototype;
    // 返回過渡對象的實例,該對象的原型繼承了父對象
    Target.prototype = new F();
    // 修正子類原型的構造函數
    Target.prototype.constructor = Target;
    // 沒法知道本身真正繼承至誰(記住最好,也不強求)
    // 爲了保存一下它的父類,也用一個 uber 來記錄一下父類
    // 由於 super 是保留字不能使用,因此使用了 uber
    Target.prototype.uber = Origin.prototype; 
}    
複製代碼

雅虎的高端寫法,採用閉包的私有化變量

var inherit = (function () {
    var F = function () {};
    return function (Target, Origin) {
        F.prototype = Origin.prototype;
        Target.prototype = new F();
        Target.prototype.constructor = Target;
        Target.prototype.uber = Origin.prototype;
    }   
}());      
複製代碼

混入方式繼承多個對象

Object.assign 會把 OtherSuperClass 原型上的方法屬性拷貝到 MyClass 原型上,使 MyClass 的全部實例均可使用 OtherSuperClass 上的方法

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 繼承一個類
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 從新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do something
};
複製代碼

ES6 類繼承 extends

ES6 的繼承和寄生組合繼承類似,本質上 ES6 繼承是 ES5 繼承的一種語法糖extends 關鍵字主要用於類聲明或類表達式中,以建立一個類表示該類是另外某個類的子類

constructor 表示構造函數,一個類中只能有一個構造函數,有多個會報出SyntaxError 錯誤,若沒有顯式指定構造方法,則會添加默認的 constructor 方法,例子以下

class Rectangle { 
   // constructor 
   constructor(height, width) { 
      this.height = height; this.width = width;
   } 
   // Getter 
   get area() { return this.calcArea() } 
   // Method 
   calcArea() { return this.height * this.width; } 
}      
const rectangle = new Rectangle(10, 20); 
console.log(rectangle.area); // 輸出 200 

// 繼承 
class Square extends Rectangle { 
   constructor(length) { 
      super(length, length); // 若是子類中存在構造函數,則須要在使用「this」以前首先調用 super()。 
      this.name = 'Square'; 
   }    
   get area() { return this.height * this.width; } 
}   
const square = new Square(10); 
console.log(square.area); // 輸出 100
複製代碼

extends 繼承的核心代碼以下,其實現和上述的寄生組合式繼承方式相似

// extends 繼承的核心代碼以下,其實現和上述的寄生組合式繼承方式類似
function _inherits(subType, superType) { 
    // 建立對象,建立父類原型的一個副本 
    // 加強對象,彌補因重寫原型而失去的默認的constructor 屬性 
    // 指定對象,將新建立的對象賦值給子類的原型 
    subType.prototype = Object.create(superType && superType.prototype, { 
        constructor: { 
            value: subType, 
            enumerable: false, 
            writable: true, 
            configurable: true 
        } 
    }); 
   
    if (superType) { 
        Object.setPrototypeOf ? 
        Object.setPrototypeOf(subType, superType) : 
        subType.__proto__ = superType; 
    } 
}   
複製代碼

總結

  • 函數聲明和類聲明的區別:函數聲明會提高,類聲明不會。首先須要聲明類而後訪問它,不然像下面的代碼會拋出一個 ReferenceError
    let p = new Rectangle(); 
    // ReferenceError
    class Rectangle {}
    複製代碼
  • ES6 Class extends 是 ES5 繼承的語法糖
  • ES5 繼承和 ES6 繼承的區別
    • ES5 的繼承實質上是先建立子類的實例對象,而後再將父類的方法添加到 this

      Child.prototype = new Parent() || Parent.apply(this) || Parent.call(this)
      複製代碼
    • ES6 的繼承有所不一樣,在 ES6 class 中,實質上是先建立父類的實例對象 this,而後再用子類的構造函數修改 this。子類必須在 constructor 方法中調用 super 方法,不然新建實例時會報錯。這是由於子類沒有本身的 this 對象而是繼承父類的 this 對象,而後對其進行加工

擴展

一道關於原型的題目

function Page() {
  return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];

const p1 = new Page();
const p2 = Page();

console.log(p1.hosts);  // undefined
console.log(p2.hosts); // Uncaught TypeError: Cannot read property 'hosts' of undefined
複製代碼

緣由分析

  • 以前文章提過 new 時若 return 了對象,則會直接拿這個對象做爲 new 的結果,所以 p1 應該是 this.hosts 的結果,而在 new Page() 時,this 是一個以 Page.prototype 爲原型的 target 對象,因此這裏 this.hosts 能夠訪問到 Page.prototype.hosts['h2']。所以 p1 就是等於 ['h2']['h2'] 沒有 hosts 屬性因此返回 undefined
  • console.log(p2.hosts) 會報錯是由於 p2 是直接調用 Page 構造函數,這個時候 this 指向全局對象,全局對象並沒 hosts 屬性,所以返回 undefined,往 undefined 上訪問 hosts 固然報錯
相關文章
相關標籤/搜索