天天看點

call stack是什麼錯誤_深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】this 的指向怎麼改變 this 的指向箭頭函數在函數内部使用 _this = this使用 apply、call、bind使用 apply使用 call使用 bindapply、call、bind 差別apply 和 call 的差別bind 和 apply、call 差別JS 中的函數調用作為一個函數調用函數作為方法調用使用構造函數調用函數作為函數方法調用函數推薦JS相關學習文章

call stack是什麼錯誤_深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】this 的指向怎麼改變 this 的指向箭頭函數在函數内部使用 _this = this使用 apply、call、bind使用 apply使用 call使用 bindapply、call、bind 差別apply 和 call 的差別bind 和 apply、call 差別JS 中的函數調用作為一個函數調用函數作為方法調用使用構造函數調用函數作為函數方法調用函數推薦JS相關學習文章

作者:sunshine小小倩

轉發連結:https://juejin.im/post/59bfe84351882531b730bac2

這又是一個面試經典問題~/(ㄒoㄒ)/~~也是 ES5中衆多坑中的一個,在 ES6 中可能會極大避免 this 産生的錯誤,但是為了一些老代碼的維護,最好還是了解一下 this 的指向和 call、apply、bind 三者的差別。

call stack是什麼錯誤_深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】this 的指向怎麼改變 this 的指向箭頭函數在函數内部使用 _this = this使用 apply、call、bind使用 apply使用 call使用 bindapply、call、bind 差別apply 和 call 的差別bind 和 apply、call 差別JS 中的函數調用作為一個函數調用函數作為方法調用使用構造函數調用函數作為函數方法調用函數推薦JS相關學習文章

前面的有幾篇文章也詳細講解來this/apply/call/bind的用法和實踐描述,不知道還有多少小夥伴回想的起來,想不起來在複習一遍:

《一文了解 this、call、apply、bind》

《手把手教你全方位解讀JS中this真正含義【實踐】》

《JavaScript ECMAScript文法概括【思維導圖】》

《關于前端174道 JavaScript知識點彙總(一)》

《關于前端174道 JavaScript知識點彙總(二)》

《關于前端174道 JavaScript知識點彙總(三)》

this 的指向

在 ES5 中,其實 this 的指向,始終堅持一個原理:this 永遠指向最後調用它的那個對象,來,跟着我朗讀三遍:this 永遠指向最後調用它的那個對象,this 永遠指向最後調用它的那個對象,this 永遠指向最後調用它的那個對象。記住這句話,this 你已經了解一半了。

下面我們來看一個最簡單的例子:例 1:

var name = "windowsName";    function a() {        var name = "Cherry";        console.log(this.name);          // windowsName        console.log("inner:" + this);    // inner: Window    }    a();    console.log("outer:" + this)         // outer: Window複制代碼
           

這個相信大家都知道為什麼 log 的是 windowsName,因為根據剛剛的那句話“this 永遠指向最後調用它的那個對象”,我們看最後調用 a 的地方 a();,前面沒有調用的對象那麼就是全局對象 window,這就相當于是 window.a();注意,這裡我們沒有使用嚴格模式,如果使用嚴格模式的話,全局對象就是 undefined,那麼就會報錯 Uncaught TypeError: Cannot read property 'name' of undefined。

再看下這個例子:例 2:

var name = "windowsName";    var a = {        name: "Cherry",        fn : function () {            console.log(this.name);      // Cherry        }    }    a.fn();複制代碼
           

在這個例子中,函數 fn 是對象 a 調用的,是以列印的值就是 a 中的 name 的值。是不是有一點清晰了呢~

我們做一個小小的改動:例 3:

var name = "windowsName";    var a = {        name: "Cherry",        fn : function () {            console.log(this.name);      // Cherry        }    }    window.a.fn();複制代碼
           

這裡列印 Cherry 的原因也是因為剛剛那句話“this 永遠指向最後調用它的那個對象”,最後調用它的對象仍然是對象 a。

我們再來看一下這個例子:例 4:

var name = "windowsName";    var a = {        // name: "Cherry",        fn : function () {            console.log(this.name);      // undefined        }    }    window.a.fn();複制代碼
           

這裡為什麼會列印 undefined 呢?這是因為正如剛剛所描述的那樣,調用 fn 的是 a 對象,也就是說 fn 的内部的 this 是對象 a,而對象 a 中并沒有對 name 進行定義,是以 log 的 this.name 的值是 undefined。

這個例子還是說明了:this 永遠指向最後調用它的那個對象,因為最後調用 fn 的對象是 a,是以就算 a 中沒有 name 這個屬性,也不會繼續向上一個對象尋找 this.name,而是直接輸出 undefined。

再來看一個比較坑的例子:例 5:

var name = "windowsName";    var a = {        name : null,        // name: "Cherry",        fn : function () {            console.log(this.name);      // windowsName        }    }    var f = a.fn;    f();複制代碼
           

這裡你可能會有疑問,為什麼不是 Cherry,這是因為雖然将 a 對象的 fn 方法指派給變量 f 了,但是沒有調用,再接着跟我念這一句話:“this 永遠指向最後調用它的那個對象”,由于剛剛的 f 并沒有調用,是以 fn() 最後仍然是被 window 調用的。是以 this 指向的也就是 window。

由以上五個例子我們可以看出,this 的指向并不是在建立的時候就可以确定的,在 es5 中,永遠是this 永遠指向最後調用它的那個對象。

再來看一個例子:例 6:

var name = "windowsName";    function fn() {        var name = 'Cherry';        innerFunction();        function innerFunction() {            console.log(this.name);      // windowsName        }    }    fn()複制代碼
           

讀到現在了應該能夠了解這是為什麼了吧(o゚▽゚)o。

怎麼改變 this 的指向

改變 this 的指向我總結有以下幾種方法:

  • 使用 ES6 的箭頭函數
  • 在函數内部使用 _this = this
  • 使用 apply、call、bind
  • new 執行個體化一個對象

例 7:

var name = "windowsName";    var a = {        name : "Cherry",        func1: function () {            console.log(this.name)             },        func2: function () {            setTimeout(  function () {                this.func1()            },100);        }    };    a.func2()     // this.func1 is not a function複制代碼
           

在不使用箭頭函數的情況下,是會報錯的,因為最後調用 setTimeout 的對象是 window,但是在 window 中并沒有 func1 函數。

我們在改變 this 指向這一節将把這個例子作為 demo 進行改造。

箭頭函數

衆所周知,ES6 的箭頭函數是可以避免 ES5 中使用 this 的坑的。箭頭函數的 this 始終指向函數定義時的 this,而非執行時。,箭頭函數需要記着這句話:“箭頭函數中沒有 this 綁定,必須通過查找作用域鍊來決定其值,如果箭頭函數被非箭頭函數包含,則 this 綁定的是最近一層非箭頭函數的 this,否則,this 為 undefined”。

例 8 :

var name = "windowsName";    var a = {        name : "Cherry",        func1: function () {            console.log(this.name)             },        func2: function () {            setTimeout( () => {                this.func1()            },100);        }    };    a.func2()     // Cherry複制代碼
           

在函數内部使用 _this = this

如果不使用 ES6,那麼這種方式應該是最簡單的不會出錯的方式了,我們是先将調用這個函數的對象儲存在變量 _this 中,然後在函數中都使用這個 _this,這樣 _this 就不會改變了。例 9:

var name = "windowsName";    var a = {        name : "Cherry",        func1: function () {            console.log(this.name)             },        func2: function () {            var _this = this;            setTimeout( function() {                _this.func1()            },100);        }    };    a.func2()       // Cherry複制代碼
           

這個例子中,在 func2 中,首先設定 var _this = this;,這裡的 this 是調用 func2 的對象 a,為了防止在 func2 中的 setTimeout 被 window 調用而導緻的在 setTimeout 中的 this 為 window。我們将 this(指向變量 a) 指派給一個變量 _this,這樣,在 func2 中我們使用 _this 就是指向對象 a 了。

使用 apply、call、bind

使用 apply、call、bind 函數也是可以改變 this 的指向的,原理稍後再講,我們先來看一下是怎麼實作的:

使用 apply

例 10:

var a = {        name : "Cherry",        func1: function () {            console.log(this.name)        },        func2: function () {            setTimeout(  function () {                this.func1()            }.apply(a),100);        }    };    a.func2()            // Cherry複制代碼
           

使用 call

例 11:

var a = {        name : "Cherry",        func1: function () {            console.log(this.name)        },        func2: function () {            setTimeout(  function () {                this.func1()            }.call(a),100);        }    };    a.func2()            // Cherry複制代碼
           

使用 bind

例 12:

var a = {        name : "Cherry",        func1: function () {            console.log(this.name)        },        func2: function () {            setTimeout(  function () {                this.func1()            }.bind(a)(),100);        }    };    a.func2()            // Cherry複制代碼
           

apply、call、bind 差別

剛剛我們已經介紹了 apply、call、bind 都是可以改變 this 的指向的,但是這三個函數稍有不同。

在 MDN 中定義 apply 如下;

apply() 方法調用一個函數, 其具有一個指定的this值,以及作為一個數組(或類似數組的對象)提供的參數

文法:

fun.apply(thisArg, [argsArray])
  • thisArg:在 fun 函數運作時指定的 this 值。需要注意的是,指定的 this 值并不一定是該函數執行時真正的 this 值,如果這個函數處于非嚴格模式下,則指定為 null 或 undefined 時會自動指向全局對象(浏覽器中就是window對象),同時值為原始值(數字,字元串,布爾值)的 this 會指向該原始值的自動包裝對象。
  • argsArray:一個數組或者類數組對象,其中的數組元素将作為單獨的參數傳給 fun 函數。如果該參數的值為null 或 undefined,則表示不需要傳入任何參數。從ECMAScript 5 開始可以使用類數組對象。浏覽器相容性請參閱本文底部内容。

apply 和 call 的差別

其實 apply 和 call 基本類似,他們的差別隻是傳入的參數不同。

call 的文法為:

fun.call(thisArg[, arg1[, arg2[, ...]]])複制代碼
           

是以 apply 和 call 的差別是 call 方法接受的是若幹個參數清單,而 apply 接收的是一個包含多個參數的數組。

例 13:

var a ={        name : "Cherry",        fn : function (a,b) {            console.log( a + b)        }    }    var b = a.fn;    b.apply(a,[1,2])     // 3複制代碼
           

例 14:

var a ={        name : "Cherry",        fn : function (a,b) {            console.log( a + b)        }    }    var b = a.fn;    b.call(a,1,2)       // 3複制代碼
           

bind 和 apply、call 差別

我們先來将剛剛的例子使用 bind 試一下

var a ={        name : "Cherry",        fn : function (a,b) {            console.log( a + b)        }    }    var b = a.fn;    b.bind(a,1,2)複制代碼
           

我們會發現并沒有輸出,這是為什麼呢,我們來看一下 MDN 上的文檔說明:

bind()方法建立一個新的函數, 當被調用時,将其this關鍵字設定為提供的值,在調用新函數時,在任何提供之前提供一個給定的參數序列。

是以我們可以看出,bind 是建立一個新的函數,我們必須要手動去調用:

var a ={        name : "Cherry",        fn : function (a,b) {            console.log( a + b)        }    }    var b = a.fn;    b.bind(a,1,2)()           // 3複制代碼
           

==================================== 更新==============================

JS 中的函數調用

看到留言說,很多童靴不了解為什麼 例 6 的 innerFunction 和 例 7 的 this 是指向 window 的,是以我就來補充一下 JS 中的函數調用。例 6:

var name = "windowsName";    function fn() {        var name = 'Cherry';        innerFunction();        function innerFunction() {            console.log(this.name);      // windowsName        }    }    fn()複制代碼
           

例 7:

var name = "windowsName";    var a = {        name : "Cherry",        func1: function () {            console.log(this.name)             },        func2: function () {            setTimeout(  function () {                this.func1()            },100);        }    };    a.func2()     // this.func1 is not a function複制代碼
           

函數調用的方法一共有 4 種

  1. 作為一個函數調用
  2. 函數作為方法調用
  3. 使用構造函數調用函數
  4. 作為函數方法調用函數(call、apply)

作為一個函數調用

比如上面的 例 1:例 1:

var name = "windowsName";    function a() {        var name = "Cherry";        console.log(this.name);          // windowsName        console.log("inner:" + this);    // inner: Window    }    a();    console.log("outer:" + this)         // outer: Window複制代碼
           

這樣一個最簡單的函數,不屬于任何一個對象,就是一個函數,這樣的情況在 JavaScript 的在浏覽器中的非嚴格模式預設是屬于全局對象 window 的,在嚴格模式,就是 undefined。

但這是一個全局的函數,很容易産生命名沖突,是以不建議這樣使用。

函數作為方法調用

是以說更多的情況是将函數作為對象的方法使用。比如例 2:例 2:

var name = "windowsName";    var a = {        name: "Cherry",        fn : function () {            console.log(this.name);      // Cherry        }    }    a.fn();複制代碼
           

這裡定義一個對象 a,對象 a 有一個屬性(name)和一個方法(fn)。

然後對象 a 通過 . 方法調用了其中的 fn 方法。

然後我們一直記住的那句話“this 永遠指向最後調用它的那個對象”,是以在 fn 中的 this 就是指向 a 的。

使用構造函數調用函數

如果函數調用前使用了 new 關鍵字, 則是調用了構造函數。這看起來就像建立了新的函數,但實際上 JavaScript 函數是重新建立的對象:
// 構造函數:function myFunction(arg1, arg2) {    this.firstName = arg1;    this.lastName  = arg2;}// This    creates a new objectvar a = new myFunction("Li","Cherry");a.lastName;                             // 傳回 "Cherry"複制代碼
           

這就有要說另一個面試經典問題:new 的過程了,(ಥ_ಥ)這裡就簡單的來看一下 new 的過程吧:僞代碼表示:

var a = new myFunction("Li","Cherry");new myFunction{    var obj = {};    obj.__proto__ = myFunction.prototype;    var result = myFunction.call(obj,"Li","Cherry");    return typeof result === 'obj'? result : obj;}複制代碼
           
  1. 建立一個空對象 obj;
  2. 将新建立的空對象的隐式原型指向其構造函數的顯示原型。
  3. 使用 call 改變 this 的指向
  4. 如果無傳回值或者傳回一個非對象值,則将 obj 傳回作為新對象;如果傳回值是一個新對象的話那麼直接直接傳回該對象。

是以我們可以看到,在 new 的過程中,我們是使用 call 改變了 this 的指向。

作為函數方法調用函數

在 JavaScript 中, 函數是對象。

JavaScript 函數有它的屬性和方法。call() 和 apply() 是預定義的函數方法。 兩個方法可用于調用函數,兩個方法的第一個參數必須是對象本身

在 JavaScript 嚴格模式(strict mode)下, 在調用函數時第一個參數會成為 this 的值, 即使該參數不是一個對象。在 JavaScript 非嚴格模式(non-strict mode)下, 如果第一個參數的值是 null 或 undefined, 它将使用全局對象替代。

這個時候我們再來看例 6:例 6:

var name = "windowsName";    function fn() {        var name = 'Cherry';        innerFunction();        function innerFunction() {            console.log(this.name);      // windowsName        }    }    fn()複制代碼
           

這裡的 innerFunction() 的調用是不是屬于第一種調用方式:作為一個函數調用(它就是作為一個函數調用的,沒有挂載在任何對象上,是以對于沒有挂載在任何對象上的函數,在非嚴格模式下 this 就是指向 window 的)

然後再看一下 例 7:例 7:

var name = "windowsName";    var a = {        name : "Cherry",        func1: function () {            console.log(this.name)             },        func2: function () {            setTimeout(  function () {                this.func1()            },100 );        }    };    a.func2()     // this.func1 is not a function複制代碼
           

這個簡單一點的了解可以了解為“匿名函數的 this 永遠指向 window”,你可以這樣想,還是那句話this 永遠指向最後調用它的那個對象,那麼我們就來找最後調用匿名函數的對象,這就很尴尬了,因為匿名函數名字啊,笑哭,是以我們是沒有辦法被其他對象調用匿名函數的。是以說 匿名函數的 this 永遠指向 window。

call stack是什麼錯誤_深入淺出講解JS中this/apply/call/bind巧妙用法【實踐】this 的指向怎麼改變 this 的指向箭頭函數在函數内部使用 _this = this使用 apply、call、bind使用 apply使用 call使用 bindapply、call、bind 差別apply 和 call 的差別bind 和 apply、call 差別JS 中的函數調用作為一個函數調用函數作為方法調用使用構造函數調用函數作為函數方法調用函數推薦JS相關學習文章

如果這個時候你要問,那匿名函數都是怎麼定義的,首先,我們通常寫的匿名函數都是自執行的,就是在匿名函數後面加 () 讓其自執行。其次就是雖然匿名函數不能被其他對象調用,但是可以被其他函數調用啊,比如例 7 中的 setTimeout。

推薦JS相關學習文章

《一文了解 this、call、apply、bind》

《手把手教你全方位解讀JS中this真正含義【實踐】》

《JavaScript ECMAScript文法概括【思維導圖】》

《關于前端174道 JavaScript知識點彙總(一)》

《關于前端174道 JavaScript知識點彙總(二)》

《關于前端174道 JavaScript知識點彙總(三)》

作者:sunshine小小倩

轉發連結:https://juejin.im/post/59bfe84351882531b730bac2

繼續閱讀