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

jsliang發表於2019-02-24

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

Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 你們的 star 是我學習的動力!GitHub 地址

本文涉及知識點

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

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

如果小夥伴對文章存有疑問,想快速得到回覆。
或者小夥伴對 jsliang 個人的前端文件庫感興趣,也想將自己的前端知識整理出來。
歡迎加 QQ 群一起探討:798961601

一 目錄

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

目錄
一 目錄
二 前言
三 題目
四 解題
五 知識擴充
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 自己)就時常懵逼在這裡。

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

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

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 世界萬物誕生記》,感興趣的小夥伴可以去了解一下。這裡我們還是看圖,並回歸本話題:

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

  • 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 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!

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

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

相關文章