JavaScript知識點:this 關鍵字到底指向誰?

前言

前端開發的小夥伴多多少少曾被 this 關鍵字難倒過,由於 JS 的 this 的指向不少時候能夠是「動態變化」的,可是關於 this 關鍵字咱們只須要記住一點:哪一個對象調用函數,函數的this指向哪一個對象前端

可是這個判斷是誰就是一個不那麼簡單的過程了,接下來咱們就一一舉例說明。面試

一、普通函數調用

1.一、基本調用

像這樣一個直接聲明的函數,它的this指向誰呢?數組

function outer(){
    console.log(this);
    
    function inner(){
        console.log(this);
    }
    inner();
}
outer();
// Window
// Window
複製代碼

上面這個例子說明直接聲明在做用域裏的函數this都指向Window對象。 可是真的是這樣嗎?接下來咱們看看這兩個例子。瀏覽器

1.二、嚴格模式

// 注: 'use strict' 只能聲明在做用域的第一行
'use strict';
function outer0(){
    console.log(this);
    
    function inner0(){
        console.log(this);
    }
    inner0();
}
outer0();
// undefined
// undefined
outer0.call(0);
// 這裏會輸出什麼?
複製代碼
function outer1(){
    console.log(this);
    
    function inner1(){
 'use strict';
        console.log(this);
    }
    inner1();
}
outer1();
// Window
// undefined
outer1.call(0);
// 這裏又會輸出什麼?
複製代碼

因此,咱們得出:直接聲明在做用域的函數,在非嚴格模式下其this指向Window對象;在嚴格模式下指向undefined 這一點並不會隨父做用域的this變化而變化。安全

小夥伴們能夠自行到瀏覽器看看經過call(thisValue)調用輸出內容,關於這點,稍後咱們也會講到。bash

接下來咱們全部代碼都默認在嚴格模式下運行app

二、對象調用

2.一、基本調用

'use strict';
var key = 20;
var value = 20;
var obj = {
    key: 10,
    fn: function(){
        console.log(this.key);
        console.log(this.value);
    }
};
obj.fn();
// 10
// undefined
複製代碼

咱們能夠看到,輸出的this.key10,而不是20;而對象上沒有value屬性,輸出的this.value獲得的是undefined,並不會由於外面聲明瞭一個value = 20就輸出20,故咱們能夠得出,經過對象調用函數,其this指向當前對象。函數

2.二、相同函數,由不一樣對象調用

可是下面這種狀況應該注意:ui

'use strict';
var obj1 = {
    key: 10,
    fn: function(){
        console.log(this.key);
        console.log(this.value);
    },
};
var obj2 = {
    key: 20,
    fn: obj1.fn,
}

obj1.fn();
// 10
// undefined
obj2.fn();
// 20
// undefined
複製代碼

雖然obj2.fn等於obj1.fn,但調用它的是obj2,因此函數this指向是obj2哪一個對象調用函數,函數裏面的this指向哪一個對象this

2.三、若是咱們把 fn 拿出來會怎樣呢?

// 這裏不能用嚴格模式了,有興趣的能夠看看使用嚴格模式會怎樣,以及爲何~~
// 'use strict';
var obj = {
    key: 10,
    fn: function(){
        console.log(this.key);
        console.log(this.value);
    },
};
var fn = obj.fn;
obj.fn();
// 10
// undefined
fn();
// undefined
// undefined
複製代碼

咱們能夠看見,當咱們拿出來單獨調用時,它輸出了兩個undefined,這是由於:哪一個對象調用函數,函數裏面的this指向哪一個對象。當咱們拿出來後,其實是由Window對象調用,進一步證實了:哪一個對象調用函數,函數裏面的this指向哪一個對象。同時也說明了,一個聲明的普通函數調用,都是由全局對象Window調用,嚴格模式下是由undefined調用。(undefined竟然能調用方法?~~~僞裝吧,但結果很重要)

三、經過構造函數調用

3.一、一般狀況:構造函數沒有return語句

'use strict';
function Student(name){
    this.name = name;
    // 當經過 new 調用時,這可認爲有一條隱式的語句
    // return this;
}

var student = new Student('lilei');
console.log(student.name);
// lilei
複製代碼

咱們都知道是這樣一個結果,可是爲何呢?這咱們就須要理清當咱們 new一個對象的時候,js 都幫咱們幹了什麼呢,其實很簡單:

// 建立一個空對象
var obj  = {};
// 將新建立的對象 __proto__ 指向 Student 的 prototype
obj.__proto__ = Student.prototype;
// 將新建立對象的指針指向 Student 函數
Student.call(obj);
複製代碼

當通過這樣一個步驟以後,當咱們執行new Student('lilei')時,咱們就知道它的this指向爲何是這麼個結果了。

等等,上面代碼提到了經過new調用時,有一個隱式return this;語句,那若是咱們顯示的寫出return語句會如何呢?

3.一、當構造函數包含return語句時

'use strict';
function Student(name){
    this.name = name;
    return {
        name: 'benshaoye',
    }
}

var student = new Student('lilei');
console.log(student.name);
// benshaoye
複製代碼

納尼,這是爲何?當咱們在構造函數裏有return時,new出來的對象就是return的對象,而不是Student對象,須要注意,不過這種狀況也挺少(我就歷來沒碰見過)。

四、經過 call、apply、bind 調用

在上面的例子中咱們已經使用過call了,apply也有相似的地方,那麼它們的做用是什麼,區別又是什麼呢?

callapplybind的第一個參數始終是函數執行時this指向的對象。

4.1 Function.prototype.call( thisValue, ...args)

call後面其餘的參數是執行函數傳給函數的參數:

'use strict';
function sum(a, b){
    return this + a + b;
}
/* * 這裏 this 指向 1,參數 a、b 分別是 二、3 */
console.log(sum.call(1, 2, 3));
// 6
複製代碼

4.2 Function.prototype.apply( thisValue, [...args])

apply只接受兩個參數,第一個參數是執行時this指向的對象,第二個參數是一個數組,數組裏的參數就是函數執行時參數,經過例子說明:

'use strict';
function sum(a, b){
    return this + a + b;
}
/* * 這裏 this 指向 1,參數 a、b 分別是 二、3 * 經過例子更能理解其中的差別 */
console.log(sum.apply(1, [2, 3]));
// 6


var obj = {
    key: 123,
    fn: function(){
        console.log(this.key);
    },
    fn0: ()=> {
        console.log(this)
    }
}
obj.fn();
// 123

// 這裏雖然調用的是 obj.fn 方法
// 可是經過 call 動態改變了它的 this 指向了 target
// 此時至關於 obj.fn 的執行環境是 target
var target = {key: 456}
obj.fn.apply(target);
// 456

// 稍後會講到,箭頭函數不能改變其 this 指向
obj.fn0.apply(target);
// undefined
複製代碼

4.3 Function.prototype.bind( thisValue, ...args)

這裏須要注意,bind的第一個參數thisValuethis指向的對象,後面的參數依次是要傳給函數的參數,而且不可修改,返回的是另一個函數,這個函數接受的參數是剩餘的參數,舉例說明:

'use strict';
function sum(a, b){
    return this + a + b;
}

var otherSum = sum.bind(1, 2);
console.log(otherSun(3));
// 6
複製代碼

能夠看出,this綁定了1,a被綁定了2,執行是3,就是b,很高級。

有興趣的小夥伴能夠先了解下 柯里化,之後也會講到。

4.4 如何本身實現callapplybind

這個問題有時候面試常常會問到,就貼出來一下,供參考。

Function.prototype.call0 = function(){
    var args = arguments, argsLen = args.length;
    var self = this;
    var customKey = 'customKey';
    // 或者 customKey = Symbol.for('fn'),這樣更安全
    var thisValue = args[0];
    thisValue[customKey] = self;
    var paramsName = [];
    for (var i = 0; i < argsLen; i++){
        params.push('args[' + i + ']');
    }
    // 這裏還有另外的方法,如:
    // self.apply(thisValue, params),不過咱們就是來實現這兩個的,是否是有點換湯不換藥的味道
    // thisValue[customKey](...params), 解構語法
    var returnVal = eval('thisValue[customKey](' + paramsName.join(',') +')');
    // 刪除咱們添加的屬性
    delete thisValue[customKey];
    return returnVal;
}

// 兩者實現很類似
Function.prototype.apply0 = function(){
    var args = arguments, argsLen = args.length;
    var self = this;
    var customKey = 'customKey';
    // 或者 customKey = Symbol.for('fn'),這樣更安全
    var thisValue = args[0];
    var params = args[1];
    thisValue[customKey] = self;
    var paramsName = [];
    for (var i = 0; i < argsLen; i++){
        params.push('params[' + i + ']');
    }
    // 其實這裏最好也用解構語法,這種方式很煩
    var returnVal = eval('thisValue[customKey](' + paramsName.join(',') +')');
    // 刪除咱們添加的屬性
    delete thisValue[customKey];
    return returnVal;
}

// 之後直接用解構語法這些新特性了,這纔是明智的選擇!!!
Function.prototype.bind0 = function(thisValue, ...args){
    const thisLen = this.length, argsLen = args.length;
    const self = this;
    if (thisLen < argsLen) {
        const params = args.slice(1, thisLen);
        return function(){
            return self.apply(thisValue, params)
        }
    } else {
        return function(...otherArgs){
            return self.apply(thisValue, args.concat(otherArgs));
        }
    }
}
複製代碼

五、箭頭函數

ES6 提供的箭頭函數,大大增長了咱們的開發效率,可是在箭頭函數裏面,是沒有this上下文的,箭頭函數裏的this是繼承外面的環境。

'use strict';

const obj1 = {
    value: 123,
    fn: function(){
        setTimeout(function(){
            // 這裏的 function 是直接聲明
            // 在調用的時候是由全局對象調用
            console.log('this.value', this.value);
        });
    }
}

const obj2 = {
    key: 456,
    fn: function(){
        // 若是說普通函數式動態綁定 this 上下文
        // 箭頭函數的 this 上下文則是靜態綁定
        // 始終指向 obj2
        const callback = () => {
            console.log('this.key', this.key);
        }
        setTimeout(callback);
        return callback;
    }
}

obj1.fn();
const obj2Arrows = obj2.fn();
obj2Arrows.call({key: 789});
// 考慮一下事件循環,執行 setTimeout 不會立刻輸出,而是在下一個循環輸出
// 若有不明白的能夠先自行查閱資料,之後也會講到這。
// 順序不必定
// this.value undefined
// this.key 456
// this.key 456
複製代碼

能夠看出,obj2Arrows經過call 執行,並不能改變其this指向,而是始終指向obj2,印證了箭頭函數this指向是靜態編譯,始終指向與其父做用域的this相同的論點。看下面例子:

function handler() {
    const fn = ()=> {
    console.log('this.value0', this.value)
    };
    fn();
    console.log('this.value1', this.value)
}

handler.call({value: 1});
handler.call({value: 2});

// this.value0 1
// this.value1 1
// this.value0 2
// this.value1 2
複製代碼

小結

  1. 直接聲明的函數直接調用時,this指向全局對象Window,嚴格模式下指向undefined;
  2. 普通函數的this始終指向調用它的那個對象;
  3. 經過callapplybind調用時,this指向其第一個參數;
  4. 箭頭函數的this是靜態編譯,始終與其父做用域的this一致;

謝謝你們,喜歡點個贊再走哦!!!

相關文章
相關標籤/搜索