2019 面試準備 - JS 原型與原型鏈

Create by jsliang on 2019-2-21 08:42:02
Recently revised in 2019-2-23 09:44:08javascript

Hello 小夥伴們,若是以爲本文還不錯,記得給個 star , 大家的 star 是我學習的動力!GitHub 地址php


【2019-08-16】Hello 小夥伴們,因爲 jsliang 對文檔庫進行了重構,這篇文章的一些連接可能失效,而 jsliang 沒有精力維護掘金這邊的舊文章,對此深感抱歉。請須要獲取最新文章的小夥伴,點擊上面的 GitHub 地址,去文檔庫查看調整後的文章。html


本文涉及知識點前端

  • prototype
  • __proto__
  • new
  • call()/apply()/bind()
  • this

在本文中,jsliang 會講解經過自我探索後關於上述知識點的我的理解,若有紕漏、疏忽或者誤解,歡迎各位小夥伴留言指出。java

若是小夥伴對文章存有疑問,想快速獲得回覆。
或者小夥伴對 jsliang 我的的前端文檔庫感興趣,也想將本身的前端知識整理出來。
歡迎加 QQ 羣一塊兒探討:798961601git

一 目錄

不折騰的前端,和鹹魚有什麼區別github

目錄
一 目錄
二 前言
三 題目
四 解題
五 知識拓展
5.1 問題少年:旅途開始
5.2 原型及原型鏈
5.3 new 爲什麼物
5.4 call() 又是啥
5.5 this 指向哪
六 總結
七 參考文獻
八 工具

二 前言

返回目錄面試

廣州小夥伴在幫我進行面試摸底的時候,提出了問題:可否談談 this 的做用?編程

題目的目的:數組

  1. 瞭解 this,說一下 this 的做用。
  2. Vue 的 this.變量,this 指向 Vue 的哪裏。(指 Vue 的實例)
  3. Vue 裏寫個 setTimeout,發現 this 改變(call()apply()=>
  4. ……大體如此……

可是,我發現了我走了一條不歸路,無心間我看了下 prototype

而後,我爬上了一座高山……

三 題目

返回目錄

相信有的小夥伴能自信地作出下面這些題~

  • 題目 1
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
  n: 2,
  m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);
複製代碼

請寫出上面編程的輸出結果是什麼?

  • 題目 2
var F = function() {};

Object.prototype.a = function() {
  console.log('a');
};

Function.prototype.b = function() {
  console.log('b');
}

var f = new F();

f.a();
f.b();

F.a();
F.b();
複製代碼

請寫出上面編程的輸出結果是什麼?

  • 題目 3
function Person(name) {
    this.name = name
}
let p = new Person('Tom');
複製代碼

問題1:1. p.__proto__等於什麼?

問題2:Person.__proto__等於什麼?

  • 題目 4
var foo = {},
    F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);
複製代碼

請寫出上面編程的輸出結果是什麼?

四 解題

返回目錄

  • 題目 1 答案:
b.n -> 1
b.m -> undefined;

c.n -> 2;
c.m -> 3;
複製代碼
  • 題目 2 答案:
f.a() -> a
f.b() -> f.b is not a function F.a() -> a F.b() -> b 複製代碼
  • 題目 3 答案

答案1:Person.prototype

答案2:Function.prototype

  • 題目 4 答案
foo.a => value a
foo.b => undefined
F.a => value a
F.b => value b
複製代碼

若是小夥伴們查看完答案,仍不知道怎麼回事,那麼,咱們擴展下本身的知識點,暢快了解更多地知識吧!

五 知識拓展

返回目錄

原型和原型鏈估計是老生常談的話題了,可是仍是有不少小白(例如 jsliang 本身)就時常懵逼在這裏。

首圖祭祖,讓暴風雨來得更猛烈些吧!

5.1 問題少年:旅途開始

返回目錄

由於愛(瞭解前因後果),因此 jsliang 開始學習(百度)之旅,瞭解原型和原型鏈。

首先jsliang 去了解查看原型鏈 prototype

而後,在瞭解途中看到了 new,因而百度查看 JS 的 new 理念。

接着,接觸 new 會了解還有 call(),而 call()apply() 以及箭頭函數 => 又是類似的東西。

最後,當咱們查找 call() 的時候,它又涉及到了 this,因此咱們 「順便」 查閱 this 吧。

5.1 原型及原型鏈

返回目錄

首先,爲何須要原型及原型鏈?

咱們查看一個例子:

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.eat = function() {
    console.log(age + "歲的" + name + "在吃飯。");
  }
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);

console.log(p1.eat === p2.eat); // false
複製代碼

能夠看到,對於同一個函數,咱們經過 new 生成出來的實例,都會開出新的一塊堆區,因此上面代碼中 person 1 和 person 2 的吃飯是不一樣的。

擁有屬於本身的東西(例如房子、汽車),這樣很好。但它也有很差,畢竟總共就那麼點地兒(內存),你不停地建房子,到最後是否是沒有空地了?(內存不足)

因此,咱要想個法子,建個相似於共享庫的對象(例如把樓房建高),這樣就能夠在須要的時候,調用一個相似共享庫的對象(社區),讓實例可以沿着某個線索去找到本身歸處。

而這個線索,在前端中就是原型鏈 prototype

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

// 經過構造函數的 Person 的 prototype 屬性找到 Person 的原型對象
Person.prototype.eat = function() {
  console.log("吃飯");
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("梁峻榮", 24);

console.log(p1.eat === p2.eat); // true
複製代碼

看!這樣咱們就經過分享的形式,讓這兩個實例對象指向相同的位置了(社區)。

而後,說到這裏,咱們就興趣來了,prototype 是什麼玩意?竟然這麼神奇!

孩子沒娘,說來話長。首先咱們要從 JavaScript 這玩意的誕生提及,可是放這裏的話,故事主線就太長了,因此這裏有個本文的劇場版《JavaScript 世界萬物誕生記》,感興趣的小夥伴能夠去了解一下。這裏咱們仍是看圖,並回歸本話題:

  • JS 說,我好寂寞。由於 JS 的本源是空的,即:null。
  • JS 說,要有神。因此它經過萬能術 __proto__ 產生了 No1 這號神,即:No1.__proto__ == null
  • JS 說,神你要有本身的想法啊。因此神本身想了個方法,根據本身的原型 prototype 建立了對象 Object,即:Object.prototype == No1; No1.__proto__ == null。因而咱們把 prototype 叫作原型,就比如 Object 的原型是神,男人的原型是人類同樣,同時 __proto__ 叫作原型鏈,畢竟有了 __proto__,對象、神、JS 之間纔有聯繫。這時候 Object.prototype.__proto__ == null
  • JS 說,神你要有更多的想法啊,我把萬能術 __proto__ 借你用了。因此神根據 Object,使用 __proto__ 作了個機器 No2,即 No2.__proto__ == No1,並規定全部的東西,經過 __proto__ 能夠鏈接機器,再找到本身,包括 Object 也是,因而 Object 成爲全部對象的原型Object.__proto__.__proto__ == No1,而後 StringNumberBooleanArray 這些物種也是如此。
  • JS 說,神你的機器好厲害喔!你的機器能不能作出更多的機器啊?神咧嘴一笑:你經過萬能術創造了我,我經過本身原型創造了對象。如此,那我造個機器 Function,Function.prototype == No2, Function.__proto__ == No2,即 Function.prototype == Function.__proto__ 吧!這樣 No2 就成了造機器的機器,它負責管理 Object、Function、String、Number、Boolean、Array 這幾個。

最後,說到這裏,咱們應該很瞭解開局祭祖的那副圖,並有點豁然開朗的感受,能清楚地瞭解下面幾條公式了:

Object.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
複製代碼

5.3 new 爲什麼物

返回目錄

這時候,咱們知道 prototype 以及 __proto__ 是啥了,讓咱們迴歸以前的代碼:

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

// 經過構造函數的 Person 的 prototype 屬性找到 Person 的原型對象
Person.prototype.eat = function() {
  console.log("吃飯");
}

let p1 = new Person("jsliang", 24);
let p2 = new Person("梁峻榮", 24);

console.log(p1.eat === p2.eat); // true
複製代碼

能夠看出,這裏有個點,咱們還不清楚,就是:new 爲什麼物?

首先,咱們來說講函數:函數分爲構造函數和普通函數

怎麼回事呢?No2 始機器 在創造機器 Function 的過程當中,創造了過多的機器,爲了方便區分這些機器,No1 神 將機器分爲兩類:創造物種類的 Function 叫作構造函數(一般面向對象),創造動做類的 Function 叫作普通函數(一般面向過程)。打個比喻:function Birl() {}function Person() {} 這類以首字母大寫形式來定義的,用來定義某個類型物種的,就叫作 構造函數。而 function fly() {}function eat() {} 這類以首字母小寫形式來定義的,用來定義某個動做的,就叫作普通函數。

注意,它們本質仍是 Function 中出來的,只是爲了方便區分,咱們如此命名

而後,咱們嘗試製做一個會飛的鳥:

// 定義鳥類
function Bird(color) {
  this.color = color;
}

// 定義飛的動做
function fly(bird) {
  console.log(bird + " 飛起來了!");
}
複製代碼

接着,咱們要使用鳥類這個機器創造一隻鳥啊,No1 神 撓撓頭,折騰了下(注意它折騰了下),跟咱們說使用 new 吧,因而:

// 定義鳥類
function Bird(color) {
  this.color = color;
}

// 創造一隻鳥
let bird1 = new Bird('藍色');

// 定義飛的動做
function fly(bird) {
  console.log(bird.color + "的鳥飛起來了!");
}

fly(bird1); // 藍色的鳥飛起來了!
複製代碼

說到這裏,咱們知道如何使用類型創造機器和動做創造機器了。

最後,咱們若是有興趣,還能夠觀察下 No1 神new 內部折騰了啥:

假如咱們使用的是:let bird1 = new Bird('藍色');

// 1. 首先有個類型機器
function ClassMachine() {
  console.log("類型創造機器");
}
// 2. 而後咱們定義一個對象物品
let thingOne = {};
// 3. 對象物品經過萬能術 __proto__ 指向了類型機器的原型(即 No 2 始機器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定義了類型機器的動做
ClassMachine.prototype.action = function(){
  console.log("動做創造機器");
}
// 6. 這個對象物品執行了動做
thingOne.action();
/* * Console: * 類型創造機器 * 動做創造機器 */
複製代碼

OK,new 作了啥,No 1 神安排地明明白白了。

那麼下面這個例子,咱們也就清楚了:

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

Person.prototype = {
  eat:function(){
    console.log('吃飯')
  },
  sleep:function(){
    console.log('睡覺')
  }
};

let p = new Person('梁峻榮',28);

// 訪問原型對象
console.log(Person.prototype);
console.log(p.__proto__); // __proto__僅用於測試,不能寫在正式代碼中

/* Console * {eat: ƒ, sleep: ƒ} * {eat: ƒ, sleep: ƒ} */
複製代碼

因此不少人會給出一條公式:

實例的 __proto__ 屬性(原型)等於其構造函數的 prototype 屬性。

如今理解地妥妥的了吧!

可是,你注意到 new 過程當中的點 4 了嗎?!!!

5.4 call() 又是啥

返回目錄

在點 4 中,咱們使用了 call() 這個方法。

那麼,call() 又是啥?

首先,咱們要知道 call() 方法是存在於 Funciton 中的,Function.prototype.callƒ call() { [native code] },小夥伴能夠去控制檯打印一下。

而後,咱們觀察下面的代碼:

function fn1() {
  console.log(1);
  this.num = 111;
  this.sayHey = function() {
    console.log("say hey.");
  }
}
function fn2() {
  console.log(2);
  this.num = 222;
  this.sayHello = function() {
    console.log("say hello.");
  }
}
fn1.call(fn2); // 1

fn1(); // 1
fn1.num; // undefined
fn1.sayHey(); // fn1.sayHey is not a function

fn2(); // 2
fn2.num; // 111
fn2.sayHello(); // fn2.sayHello is not a function

fn2.sayHey(); //say hey.
複製代碼

經過 fn1.call(fn2),咱們發現 fn1fn2 都被改變了,call() 就比如一個小三,破壞了 fn1fn2 和氣的家庭。

如今,fn1 除了打印本身的 console,其餘的一無全部。而 fn2 擁有了 fn1 console 以外的全部東西:num 以及 sayHello

記住:在這裏,call() 改變了 this 的指向。

而後,咱們應該順勢看下它源碼,搞懂它究竟怎麼實現的,可是 jsliang 太菜,看不懂網上關於它源碼流程的文章,因此我們仍是多上幾個例子,搞懂 call() 能作啥吧~

  • 例子 1:
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

let food1 = new Food('chees', 5);

food1; // Food {name: "chees", price: 5, category: "food"}
複製代碼

能夠看出,經過在 Food 構造方法裏面調用 call(),成功使 Food 拓展了 name 以及 price 這兩個字段。因此:

準則一:可使用 call() 方法調用父構造函數。

  • 例子 2:
var animals = [
  {
    species: 'Lion',
    name: 'King'
  },
  {
    species: 'Whale',
    name: 'Fail'
  }
]

for(var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species + ": " + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

// #0 Lion: King
// #1 Whale: Fail
複製代碼

能夠看到,在匿名函數中,咱們經過 call(),成功將 animals中的 this 指向到了匿名函數中,從而循環打印出了值。

準則二:使用 call() 方法調用匿名函數。

  • 例子 3:
function greet() {
  var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
  console.log(reply);
}

var obj = {
  animal: 'cats',
  sleepDuration: '12 and 16 hours'
};

greet.call(obj);  // cats typically sleep between 12 and 16 hours
複製代碼

準則三:使用 call() 方法調用函數而且指定上下文的 this

最後,講到這裏,小夥伴們應該知道 call() 的一些用途了。

說到 call(),咱們還要講講跟它類似的 apply(),其實這二者都是類似的,只是 apply() 調用的方式不一樣,例如:

function add(a, b){
  return a + b;  
}
function sub(a, b){
  return a - b;  
}

// apply() 的用法
var a1 = add.apply(sub, [4, 2]); // sub 調用 add 的方法
var a2 = sub.apply(add, [4, 2]);

a1; // 6 
a2; // 2

// call() 的用法
var a1 = add.call(sub, 4, 2);
複製代碼

是的,apply() 只能調用兩個參數:新 this 對象和一個數組 argArray。即:function.call(thisObj, [arg1, arg2]);

以上,咱們知道 apply()call() 都是爲了改變某個函數運行時的上下文而存在的(就是爲了改變函數內部的 this 指向)。而後,由於這兩個方法會當即調用,因此爲了彌補它們的缺失,還有個方法 bind(),它不會當即調用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>call()、apply() 以及 bind()</title>
</head>
<body>
  <div id="box">我是一個盒子!</div>
  
  <script> window.onload = function() { var fn = { num: 2, fun: function() { document.getElementById("box").onclick = (function() { console.log(this.num); }).bind(this); // }).call(this); // }).apply(this); } /* * 這裏的 this 是 fun,因此能夠正確地訪問 num, * 若是使用 bind(),會在點擊以後打印 2; * 若是使用 call() 或者 apply(),那麼在刷新網頁的時候就會打印 2 */ } fn.fun(); } </script>
</body>
</html>
複製代碼

再回想下,爲何會有 call()apply() 呢,咱們還會發現它牽扯上了 this 以及箭頭函數(=>),因此下面咱們來了解了解吧~

5.5 this 指向哪

返回目錄

  • 在絕大多數狀況下,函數的調用方式決定了 this 的值。它在全局執行環境中 this 都指向全局對象

怎麼理解呢,咱們舉個例子:

// 在瀏覽器中, window 對象同時也是全局對象
conosle.log(this === window); // true

a = 'apple';
conosle.log(window.a); // apple

this.b = "banana";
console.log(window.b); // banana
console.log(b); // banana
複製代碼

可是,平常工做中,大多數的 this,都是在函數內部被調用的,而:

  • 在函數內部,this 的值取決於函數被調用的方式。
function showAge(age) {
  this.newAge = age;
  console.log(newAge);
}
showAge("24"); // 24
複製代碼

然而,問題總會有的:

  • 通常 this 指向問題,會發生在回調函數中。因此咱們在寫回調函數時,要注意一下 this 的指向問題。
var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995;
    var fn = function() {
      return this.birth; 
      // this 指向被改變了!
      // 由於這裏從新定義了個 function,
      // 假設它內部有屬於本身的 this1,
      // 而後 getAge 的 this 爲 this2,
      // 那麼,fn 固然奉行就近原則,使用本身的 this,即:this1
    };
    return fn();
  }
}

obj.getAge(); // undefined
複製代碼

在這裏咱們能夠看到, fn 中的 this 指向變成 undefined 了。

固然,咱們是有補救措施的。

首先,咱們使用上面說起的 call()

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var fn = function() {
      return this.birth; 
    };
    return fn.call(obj); // 經過 call(),將 obj 的 this 指向了 fn 中
  }
}

obj.getAge(); // 1995
複製代碼

而後,咱們使用 that 來接盤 this

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var that = this; // 將 this 指向丟給 that
    var fn = function() {
      return that.birth; // 經過 that 來尋找到 birth
    };
    return fn();
  }
}

obj.getAge(); // 1995
複製代碼

咱們經過了 var that = this,成功在 fn 中引用到了 objbirth

最後,咱們還可使用箭頭函數 =>

var obj = {
  birth: 1995,
  getAge: function() {
    var b = this.birth; // 1995
    var fn = () => this.birth;
    return fn();
  }
}
obj.getAge(); // 1995
複製代碼

講到這裏,咱們再回首 new 那塊咱們不懂的代碼:

// 1. 首先有個類型機器
function ClassMachine() {
  console.log("類型創造機器");
}
// 2. 而後咱們定義一個對象物品
let thingOne = {};
// 3. 對象物品經過萬能術 __proto__ 指向了類型機器的原型(即 No 2 始機器)
thingOne.__proto__ = ClassMachine.prototype;
// 4. ???
ClassMachine.call(thingOne);
// 5. 定義了類型機器的動做
ClassMachine.prototype.action = function(){
  console.log("動做創造機器");
}
// 6. 這個對象物品執行了動做
thingOne.action();
/* * Console: * 類型創造機器 * 動做創造機器 */
複製代碼

很容易理解啊,在第四步中,咱們將 ClassMachinethis 變成了 thingOnethis 了!

以上,是否是感受鬼門關走了一遭,終於成功見到了曙光!!!

六 總結

返回目錄

在開始的時候,也許有的小夥伴,看着看着會迷暈了本身!

沒關係,我也是!

當我跟着本身的思路,一步一步敲下來以後,我才發覺本身彷彿打通了任督二脈,對一些題目有了本身的理解。

因此,最重要的仍是 折騰 啦!

畢竟:

不折騰的前端,和鹹魚有什麼區別!

七 參考資料

返回目錄

下面列舉本文精選參考文章,其中一些不重要的零零散散 30 來篇文章已被刷選。

八 工具

返回目錄


jsliang 廣告推送:
也許小夥伴想了解下雲服務器
或者小夥伴想買一臺雲服務器
或者小夥伴須要續費雲服務器
歡迎點擊 雲服務器推廣 查看!

知識共享許可協議
jsliang 的文檔庫梁峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的做品創做。
本許可協議受權以外的使用權限能夠從 creativecommons.org/licenses/by… 處得到。

相關文章
相關標籤/搜索