字節跳動面試官—麻煩你搞個方法出來🌈使得如下程序最後能輸出 success

前言

本文會以詳細講解一道 字節面試題 的方式,按部就班徹底搞定 js 中 this 指向優先級的問題。 ⛹️‍♂️⛹️‍♂️
js 中的 this 指向問題應該是一個討論了好久的話題了,關於這個話題的文章,在掘金也有不少。可是,可能以前看到的文章不怎麼適合本身,每次看完都仍是似懂非懂、沒有多少頭緒。前幾天幸得個人老學長—— 猛哥 的交流以後,好像對這個問題理解的更深了些,寫篇文章總結一下。🌈前端

只要你仔細認真看完這篇文章,無論你是 js 新手、仍是大佬,我保證你必定會有收穫的!若是一丟丟收穫都沒有,你能夠揍我!️👀
複製代碼

話很少說上題先

  • 請手寫實現 ES5 中 Function 原型的 bind(即手寫下面代碼塊中的myBind) 方法,使得如下程序最後能輸出 'success'
function Animal(name, color) {
  this.name = name;
  this.color = color;
}
Animal.prototype.say = function () {
  return `I'm a ${this.color} ${this.name}`;
};
const Cat = Animal.myBind(null, 'cat');
const cat = new Cat('white');
if (cat.say() === 'I\'m a white cat' &&
  cat instanceof Cat && cat instanceof Animal) {
  console.log('success');
}
複製代碼
先來看看解題須要瞭解的一些問題 慢慢來 不要慌張🌈
複製代碼

相關知識

要理解這道題,必須先掌握一些相關知識

1、js 中 this 的指向

  • 在 ES5 中, this 的指向始終是一個原則:this 的指向並非在建立的時候就能夠肯定的,在 es5 中, this 永遠指向 最後調用它的那個對象

舉幾個例子證實一下上述觀點

oneweb

var name = "globalName";
function a() {
  var name = "jingjing";
  console.log(this.name)
}
a();    //globalName
複製代碼

根據剛剛上面那個原則: this 永遠指向 最後調用它的那個對象 能夠獲得答案。咱們看最後調用 a 的地方是在哪裏?在最後一行代碼a(); 它前面沒有調用的對象,那麼就是默認的全局對象 window,因此console.log(this.name)就變成了console.log(window.name),結果輸出的是 globalName(👉非嚴格模式下👈)。面試

two

var name = "globalName";
var a = {
  name: "jingjing",
  jing: function () {
    console.log(this.name);  // jingjing
  }
}
window.a.jing();
複製代碼

我又要重複上面那句話了😁。 this 永遠指向 最後調用它的那個對象 能夠獲得答案。咱們看最後調用 jing() 函數 的地方是在哪裏?或者說函數 jing() 左邊這個.的左邊的對象是哪一個?顯然是對象 a,因此console.log(this.name)就變成了console.log(a.name),結果輸出的是jingjing微信

three

var name = "globalName";
var a = {
  name: "jingjing",
  jing: function () {
    console.log(this.name);  // globalName
  }
}
var hao = a.jing
hao();
複製代碼

🎈 這裏咱們雖然將 a 對象的 jing 方法賦值給變量 hao 了,可是注意!!!🔍這一步沒有調用執行 jing 方法!。代碼最後一行 hao() 才被 window調用執行了 jing()方法。 因此console.log(this.name)就變成了console.log(window.name),結果輸出的是 globalName。再拿出這個原則:this 永遠指向最後調用它的那個對象。🌈🌈🌈markdown

2、this 的四種綁定規則

在 JavaScript 中,this 指向的綁定規則有如下四種:app

  • 默認綁定(非嚴格模式狀況下,this 指向 window, 嚴格模式下,this指向 undefined。)
  • 隱式綁定(若是函數調用時,前面存在調用它的對象,那麼this就會隱式綁定到這個對象上)
  • 顯式綁定(函數經過 call()apply()bind()調用,this 指向被綁定的對象。)
  • new 綁定(函數被 new 調用,this 指向由 new 新構造出來的這個對象。)

上文對前兩種已經有所理解,如今咱們來聚光到後兩種。

顯式綁定

這種綁定方式就是使用 Function.prototype 中的三個方法 call()apply(),和 bind() 了。這三個函數,均可以改變函數的 this 指向到指定的對象,不一樣之處在於:svg

  • call()apply() 都是 當即執行函數 ,可是它們接受的參數的形式不一樣,具體以下:
call(this, arg1, arg2, ...)
apply(this, [arg1, arg2, ...])
複製代碼
  • bind() 則是 返回一個新的包裝函數,而不是馬上執行。bind()會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲它運行時的 this,以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。
bind(this, arg1, arg2, ...)
複製代碼

new 綁定

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。函數

  • 1.建立(或者說構造)一個全新的對象。
  • 2.這個新對象會被執行[[Prototype]]鏈接。
  • 3.這個新對象會綁定到函數調用的this。
  • 4.若是函數沒有返回其餘對象,那麼new表達式中的函數調用會自動返回這個新對象。

在 JavaScript 中,new 操做符並不像其餘面向對象的語言同樣,而是一種模擬出來的機制。在 JavaScript 中,全部的函數均可以被 new 調用,這時候這個函數通常會被稱爲 「構造函數」,實際上並不存在所謂「構造函數」,更確切的理解應該是對於函數的 「構造器調用模式」。oop

以上關於 new 綁定來源於你不知道的js上卷 2.2綁定規則。

3、綁定規則的優先級

new 綁定 > 顯式綁定 > 隱式綁定 > 默認綁定學習

結論如上,下面給一些例子證實這個結論。

毫無疑問,默認綁定的優先級是四條規則中最低的,因此咱們能夠先不考慮它。
複製代碼

one

function jing() {
  console.log(this.a);
}

var obj1 = {
  a: 10,
  foo: jing
};

var obj2 = {
  a: 20,
  foo: jing
};

obj1.foo();   //10
obj2.foo();   //20

obj1.foo.call(obj2);   //20
obj2.foo.call(obj1);   //10
複製代碼

由這個運行結果可知,上面代碼塊倒數兩行經過 call() 方法改變了 this 的指向。因此能夠獲得 顯式綁定 > 隱式綁定 這個結論。

two

function jing() {
  this.a = 'hao';
}

let obj = {
  a: 'jing'
};
// 一、bind
const Bar = jing.bind(obj);
// 二、new
const bar = new Bar();
console.log(obj.a, '--', bar.a) //jing -- hao
複製代碼

上面代碼塊倒數第三行經過 bind() 方法改變了 this 的指向obj,由上面這個 bar.a 打印輸出結果爲 hao 可知,倒數第二行代碼改變了this指向 jing(),因此能夠獲得 new綁定 > 顯式綁定

因此最後能夠有此結論: new 綁定 > 顯式綁定 > 隱式綁定 > 默認綁定


4、手寫一個簡易版的 bind()

代碼以下:

Function.prototype.myBind = function (thisObj, ...arg1) {
  // 一、這裏 ...arg1 是第一次傳的參數
  let fn = this;
  // 這裏用 fn 記下調用對象
  function jingbind(...arg2) {
    // 二、這裏 ...arg2 是下一次傳的參數
    const args = arg1.concat(arg2);
    // 誰調用 bind,最終拼好的參數就傳給誰
    return fn.apply(thisObj, args);
  }
  return jingbind;
  // bind() 返回一個未執行的函數
}
複製代碼

有了一個手寫的 myBind(),咱們再寫一個測試代碼來試試效果:

function sum(a, b, c) {
  return a + b + c;
}

const sum10 = sum.myBind(null, 10);
let jing = sum10(20, 30);
console.log(jing)   //60
複製代碼

🙌🙌通過測試,成功輸出正確結果。那咱們如今能夠試試咱們剛剛實現的 myBind() 方法能不能解決文章頂部拋出來的面試題!想一想都讓人很興奮!!🙋‍♂️🙋‍♂️

解題過程

把上面那個方法和給出的題目放在一塊兒,執行一下試試。

Function.prototype.myBind = function (thisObj, ...arg1) {
  if (typeof this !== "function") {
    throw new TypeError("not a function");
  }
  // 一、這裏 ...arg1 是第一次傳的參數
  let fn = this;
  // 這裏用 fn 記下調用對象
  function jingbind(...arg2) {
    // 二、這裏 ...arg2 是下一次傳的參數
    const args = arg1.concat(arg2);
    // 誰調用 bind,最終拼好的參數就傳給誰
    return fn.apply(thisObj, args);
  }
  return jingbind;
  // bind() 返回一個未執行的函數
}

function Animal(name, color) {
  this.name = name;
  this.color = color;
}
Animal.prototype.say = function () {
  return `I'm a ${this.color} ${this.name}`;
};
const Cat = Animal.myBind(null, 'cat');
const cat = new Cat('white');
console.log(cat.say())  //這句代碼是做者解題調試加上去的
if (cat.say() === 'I\'m a white cat' &&
  cat instanceof Cat && cat instanceof Animal) {
  console.log('success');
}
複製代碼

很遺憾、也很正常,報錯了。錯誤信息截圖以下:

  • 報錯信息顯示說 cat.say() is not a function,那它爲何不是一個方法呢?
  • 咱們去代碼中找下這個 cat.say(),一步一步往上追。
  • 這個 cat 是哪裏來的?是從 Catnew 出來的實例,那這個 Cat 又是從哪裏來的呢?是由 Animal.myBind 生成的,你調用了 myBind() 我給你返回的。那 myBind()返回了什麼呢?它返回的是 jingbind()。因此最後咱們能獲得Cat === jingbind(),便可以獲得 cat === new jingbind()
  • 那咱們發現咱們寫的這個 jingbind() 函數裏面沒有定義一個 say() 方法啊, 這個 say() 方法定義在 Animal.prototype上面。
  • 也就是說本來 Animal.prototype 上面有一個 say() 方法,可是通過咱們寫的 myBind() 方法處理後,把Animal.prototype搞丟了。
  • 因此咱們必須給它加上去,那怎麼加呢?修改的 myBind() 代碼以下:
Function.prototype.myBind = function (thisObj, ...arg1) {
  if (typeof this !== "function") {
    throw new TypeError("not a function");
  }
  // 一、這裏 ...arg1 是第一次傳的參數
  let fn = this;
  // 這裏用 fn 記下調用對象
  function jingbind(...arg2) {
    // 二、這裏 ...arg2 是下一次傳的參數
    const args = arg1.concat(arg2);
    // 誰調用 bind,最終拼好的參數就傳給誰
    return fn.apply(thisObj, args);
  }
  jingbind.prototype = fn.prototype;    //只添加了這一行代碼
  return jingbind;
  // bind() 返回一個未執行的函數
}
複製代碼

再執行一下試試

😎😎😎

  • 代碼不報錯說明原型已經加上去了。
  • 可是這個輸出結果說明咱們剛成功把原型加上去,又發現咱們沒有正常傳參數進去。🤕🤕🤕
  • 由於 this 沒有綁定到 cat 上去

這裏就涉及到前面講到的 js 中 this 綁定規則的優先級問題了。咱們寫的 myBind() 函數裏面沒有作優先級的判斷 (換句話說就是沒有對不一樣的 this 綁定規則作出相應的 this 綁定)。


把 myBind() 變完美

咱們對 myBind() 方法再作出一些改變以下:

Function.prototype.myBind = function (thisObj, ...arg1) {
  if (typeof this !== "function") {
    throw new TypeError("not a function");
  }
  // 一、這裏 ...arg1 是第一次傳的參數
  let fn = this;
  // 這裏用 fn 記下調用對象
  function jingbind(...arg2) {
    // 二、這裏 ...arg2 是下一次傳的參數
    const args = arg1.concat(arg2);
    // 誰調用 bind,最終拼好的參數就傳給誰
    let isjing = this instanceof jingbind;  //判斷是不是 new 調用
    return fn.apply(isjing ? this : thisObj, args);
  }
  jingbind.prototype = fn.prototype;
  return jingbind;
  // bind() 返回一個未執行的函數
}
複製代碼
  • instanceof 的做用沒必要多說
  • 咱們利用instanceof判斷是否是經過 new 調用的,若是是 new 調用的 咱們就要把 this 綁定到實例上去。
  • 若是不是 new 調用的,咱們就讓 this 綁定到 myBind() 函數的第一個參數——thisObj,這樣處理一下咱們應該能拿到想要的結果吧🤐🤐
再來測試一下🥱
複製代碼

最後的最後,咱們把這個方法搞出來了。💦💦💦

最後

  • 此題來源於個人老學長 猛哥 的面試題,非做者的面試題(仍是🥦💪)。
  • 我猛哥說了,認真仔細把這題搞清楚了,js 的 this 相關問題差很少都能理解了。
  • 上述各類觀點都是做者寫的,不懂得也是看書籍總結的。
  • 做者是一名入門前端不久的學生,因此文章可能會有一些錯誤,若是有大佬看到指出糾正一下,讓大白多一個學習機會,那纔是真大佬😜

做者搭建了一個博客網站,準備從基礎開始,不斷更新,搭建完備的知識體系,爲之後的面試作準備。若是是剛開始入門的小夥伴,有須要能夠來看看哦🙋‍♂️🙋‍♂️

婧婧的成長之路

最後的最後

有任何問題歡迎加做者微信交流學習(大佬忽略👀)

相關文章
相關標籤/搜索