欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文徹底理解JavaScript原型與原型鏈

 更新時間:2022年08月17日 09:33:18   作者:夏日  
這篇文章主要介紹了一文徹底理解JavaScript原型與原型鏈,JavaScript中有許多內(nèi)置對象,如:Object,?Math,?Date等,文章圍繞主題展開主題詳情,具有一定的參考價值,需要的小伙伴可以參考一下

前言

JavaScript中有許多內(nèi)置對象,如:Object, Math, Date等。我們通常會這樣使用它們:

// 創(chuàng)建一個JavaScript Date實例
const date = new Date();
// 調(diào)用getFullYear方法,返回日期對象對應的年份
date.getFullYear();
// 調(diào)用Date的now方法
// 返回自1970-1-1 00:00:00 UTC(世界標準時間)至今所經(jīng)過的毫秒數(shù)
Date.now()

當然,我們也可以自己創(chuàng)建自定義對象:

function Person() {
    this.name = '張三';
    this.age = 18;
}
Person.prototype.say = function() {
    console.log('say');
}
const person = new Person();
person.name; // 張三
person.say(); // say

看到這些代碼,不知道你是否有這些疑問:

  • new關(guān)鍵執(zhí)行函數(shù)和普通函數(shù)執(zhí)行有什么區(qū)別嗎?
  • 對象的實例為什么可以調(diào)用構(gòu)造函數(shù)的原型方法,它們之間有什么關(guān)系嗎?

接下來,讓我們帶著這些問題一步步深入學習。

new對函數(shù)做了什么?

當我們使用new關(guān)鍵字執(zhí)行一個函數(shù)時,除了具有函數(shù)直接執(zhí)行的所有特性之外,new還幫我們做了如下的事情:

  • 創(chuàng)建一個空的簡單JavaScript對象(即{})
  • 將空對象的__proto__連接到(賦值為)該函數(shù)的prototype
  • 將函數(shù)的this指向新創(chuàng)建的對象
  • 函數(shù)中如果沒有返回對象的話,將this作為返回值

用代碼表示大概是這樣:

// 1. 創(chuàng)建空的簡單js對象
const plainObject = {};
// 2. 將空對象的__proto__連接到該函數(shù)的prototype
plainObject.__proto__ = function.prototype;
// 3. 將函數(shù)的this指向新創(chuàng)建的對象
this = plainObject;
// 4. 返回this
return this

可以看到,當我們使用new執(zhí)行函數(shù)的時候,new會幫我們在函數(shù)內(nèi)部加工this,最終將this作為實例返回給我們,可以方便我們調(diào)用其中的屬性和方法。

下面,我們嘗試實現(xiàn)一下new:

function _new (Constructor, ...args) {
  // const plainObject = {};
  // plainObject.__proto__ = constructor.prototype;
  // __proto__在有些瀏覽器中不支持,而且JavaScript也不推薦直接使用該屬性
  // Object.create: 創(chuàng)建一個新對象,使用現(xiàn)有的對象提供新創(chuàng)建的對象的__proto__
  const plainObject = Object.create(Constructor.prototype);
  // 將this指向新創(chuàng)建的對象
  const result = Constructor.call(plainObject, ...args);
  const isObject = result !== null && typeof result === 'object' || typeof result === 'function';
  // 如果返回值不是對象的話,返回this(這里是plainObject)
  return isObject ? result : plainObject;
}

簡單用一下我們實現(xiàn)的_new方法:

function Animal (name) {
  this.name = name;
  this.age = 2;
}

Animal.prototype.say = function () {
  console.log('say');
};

const animal = new Animal('Panda');
console.log(animal.name); // Panda
animal.say(); // say

在介紹new的時候,我們提到了prototype,__proto__這些屬性。你可能會疑惑這些屬性的具體用途,別急,我們馬上進行介紹!

原型和原型鏈

在學習原型和原型鏈之前,我們需要首先掌握以下三個屬性:

  • prototype: 每一個函數(shù)都有一個特殊的屬性,叫做原型(prototype)
  • constructor: 相比于普通對象的屬性,prototype屬性本身會有一個屬性constructor,該屬性的值為prototype所在的函數(shù)
  • __proto__: 每一個對象都有一個__proto__屬性,該屬性指向?qū)ο?實例)所屬構(gòu)造函數(shù)(類)的原型prototype

以上的解釋只針對于JavaScript語言

我們再來看下邊的一個例子:

function Fn () {
  this.x = 100;
  this.y = 200;
  this.getX = function () {
    console.log(this.x);
  };
}

Fn.prototype.getX = function () {
  console.log(this.x);
};

Fn.prototype.getY = function () {
  console.log(this.y);
};

const fn = new Fn()

我們畫圖來描述一下上邊代碼中實例、構(gòu)造函數(shù)、以及prototype__proto__之間的關(guān)系:

我們再來看一下FunctionObject以及其原型之間的關(guān)系:

由于FunctionObject都是函數(shù),因此它們的所屬類為Function,它們的__proto__都指向Function.prototype。而Function.prototype.__proto__又指向Object.prototype,所以它們既可以調(diào)用函數(shù)原型上的方法,也可以調(diào)用對象原型上的方法。

當我們需要獲取實例上的某個屬性時:

上例中:
  • 實例:fn
  • 實例所屬類: Fn
  • 首先會從自身的私有屬性上進行查找
  • 如果沒有找到,會到自身的__proto__上進行查找,而實例的__proto__指向其所屬類的prototype,即會在類的prototype上進行查找
  • 如果還沒有找到,繼續(xù)到類的prototype__proto__中查找,即Object.prototype
  • 如果在Object.prototype中依舊沒有找到,那么返回null

上述查找過程便形成了JavaScript中的原型鏈。

在理解了原型鏈和原型的指向關(guān)系后,我們看看以下代碼會輸出什么:

const f1 = new Fn();
const f2 = new Fn();
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);

console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);

f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
// false
// true

// true
// false
// false
// Fn
// Object

// 100
// undefined
// 200
// undefined

到這里,我們已經(jīng)初步理解了原型和原型鏈的一些相關(guān)概念,下面讓我們通過一些實際例子來應用一下吧!

借用原型方法

JavaScript中,我們可以通過call/bind/apply來更改函數(shù)中this指向,原型上方法的this也可以通過這些api來進行更改。比如我們要將一個偽數(shù)組轉(zhuǎn)換為真實數(shù)組,可以這樣做:

function fn() {
  return Array.prototype.slice.call(arguments)
}
fn(1,2,3) // [ 1, 2, 3]

這里我們使用arguments調(diào)用了數(shù)組原型上的slice,這是怎么做到的呢?我們先簡單模擬下slice方法的實現(xiàn):

arguments是一個類似數(shù)組的對象,有length屬性和從零開始的索引,它可以調(diào)用Object.prototype上的方法,但是不能調(diào)用Array.prototype上的方法。

Array.prototype.mySlice = function (start = 0, end = this.length) {
  const array = [];
  // 一般會通過Array的實例(數(shù)組)調(diào)用該方法,所以this指向調(diào)用該方法的數(shù)組
  // 這里我們將this指向了arguments = {0: 1, 1: 2, 2: 3, length: 3}
  for (let i = 0; i < end; i++) {
    array[i] = this[i];
  }
  return array;
};

function fn () {
  return Array.prototype.mySlice.call(arguments);
}

console.log(fn(1, 2, 3)); // [1, 2, 3]

可能你想直接調(diào)用arguments.slice()方法,但是遺憾的是arguments是一個對象,不能調(diào)用數(shù)組原型上的方法。

當我們將Array.prototype.slice方法的this指向arguments對象時,由于arguments擁有索引屬性以及length屬性,所以可以像數(shù)組一樣根據(jù)length和索引來進行遍歷,從而相當于用arguments調(diào)用了數(shù)組原型上的方法。

下面是另一個借用原型方法常見的例子:

Object.prototype.toString.call([1,2,3]) // [object Array]
Object.prototype.toString.call(function() {}) // [object Number]

這里將Object.prototype.toStringthis由對象(Object的實例)改為了數(shù)組(Array的實例)和函數(shù)(Function的實例),相當于為數(shù)組和函數(shù)調(diào)用了對象上的toString方法,而不是調(diào)用它們自身的toString方法。

通過借用原型方法,我們可以讓變量調(diào)用自身以及自己原型上沒有的方法,增加了代碼的靈活性,也避免了一些不必要的重復工作。

實現(xiàn)構(gòu)造函數(shù)之間的繼承

通過JavaScript中的原型和原型鏈,我們可以實現(xiàn)構(gòu)造函數(shù)的繼承關(guān)系。假設(shè)有如下A,B倆個構(gòu)造函數(shù):

function A () {
  this.a = 100;
}

A.prototype.getA = function () {
  console.log(this.a);
};

function B () {
  this.b = 200;
}

B.prototype.getB = function () {
  console.log(this.b);
};

方案一

這里我們可以讓B.prototype成為A的實例,那么B.prototype中就擁有了私有方法a,以及原型對象上的方法B.prototype.__proto__A.prototype上的方法getA。最后記得要修正B.prototypeconstructor屬性,因為此時它變成了B.prototype.constructor,也就是B

function A () {
  this.a = 100;
}

A.prototype.getA = function () {
  console.log(this.a);
};
B.prototype = new A();
B.prototype.constructor = B;
function B () {
  this.b = 200;
}

B.prototype.getB = function () {
  console.log(this.b);
};

畫圖理解一下:

下面我們創(chuàng)建B的實例,看下是否成功繼承了A中的屬性和方法。

const b = new B();
console.log('b', b.a);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200

方案二

我們也可以通過將父構(gòu)造函數(shù)當做普通函數(shù)來執(zhí)行,并通過call指定this,從而實現(xiàn)實例自身屬性的繼承,然后再通過Object.create指定子構(gòu)造函數(shù)的原型對象。

function A () {
  this.a = 100;
}

A.prototype.getA = function () {
  console.log(this.a);
};
// 繼承原型方法
// 創(chuàng)建一個新對象,使用一個已經(jīng)存在的對象作為新創(chuàng)建對象的原型
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function B () {
  // 繼承私有方法
  A.call(this); // 如果有參數(shù)的話可以在這里傳入
  this.b = 200;
}

B.prototype.getB = function () {
  console.log(this.b);
};

這里我們再次通過畫圖的形式梳理一下邏輯:

下面我們創(chuàng)建B的實例,看下是否成功繼承了A中的屬性和方法。

const b = new B();
console.log('b', b.a);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200

class extends實現(xiàn)繼承

es6中為開發(fā)者提供了extends關(guān)鍵字,可以很方便的實現(xiàn)類之間的繼承:

function A () {
  this.a = 100;
}

A.prototype.getA = function () {
  console.log(this.a);
};
// 繼承原型方法
// 創(chuàng)建一個新對象,使用一個已經(jīng)存在的對象作為新創(chuàng)建對象的原型
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function B () {
  // 繼承私有方法
  A.call(this); // 如果有參數(shù)的話可以在這里傳入
  this.b = 200;
}

B.prototype.getB = function () {
  console.log(this.b);
};

下面我們創(chuàng)建B的實例,看下是否成功繼承了A中的屬性和方法。

const b = new B();
console.log('b', b.a);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200

大家可能會好奇classextends關(guān)鍵字是如何實現(xiàn)繼承的呢?下面我們用babel 編譯代碼,看下其源碼中比較重要的幾個點:

看下這倆個方法的實現(xiàn):

值得留意的一個地方是:extends將父類的靜態(tài)方法也繼承到了子類中

class A {
  constructor () {
    this.a = 100;
  }

  getA () {
    console.log(this.a);
  }
}

A.say = function () {
  console.log('say');
};

class B extends A {
  constructor () {
    // 繼承私有方法
    super();
    this.b = 200;
  }

  getB () {
    console.log(this.b);
  }
}
B.say(); // say

extends的實現(xiàn)類似于方案二:

  • apply方法更改父類this指向,繼承私有屬性
  • Object.create繼承原型屬性
  • Object.setPrototypeOf繼承靜態(tài)屬性

結(jié)語

理解JavaScript的原型原型鏈可能并不會直接提升你的JavaScrit編程能力,但是它可以幫助我們更好的理解JavaScript中一些知識點,想明白一些之前不太理解的東西。在各個流行庫或者框架中也有對于原型或原型鏈的相關(guān)應用,學習這些知識也可以為我們閱讀框架源碼奠定一些基礎(chǔ)。

到此這篇關(guān)于一文徹底理解JavaScript原型與原型鏈的文章就介紹到這了,更多相關(guān)JS原型與原型鏈內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論