阅读 514

JavaScript知识点:this 关键字到底指向谁?

前言

前端开发的小伙伴多多少少曾被 this 关键字难倒过,因为 JS 的 this 的指向很多时候可以是“动态变化”的,但是关于 this 关键字我们只需要记住一点:哪个对象调用函数,函数的this指向哪个对象

但是这个判断是谁就是一个不那么简单的过程了,接下来我们就一一举例说明。

1、普通函数调用

1.1、基本调用

像这样一个直接声明的函数,它的this指向谁呢?

function outer(){
    console.log(this);
    
    function inner(){
        console.log(this);
    }
    inner();
}
outer();
// Window
// Window
复制代码

上面这个例子说明直接声明在作用域里的函数this都指向Window对象。 但是真的是这样吗?接下来我们看看这两个例子。

1.2、严格模式

// 注: '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)调用输出内容,关于这点,稍后我们也会讲到。

接下来我们所有代码都默认在严格模式下运行

2、对象调用

2.1、基本调用

'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.2、相同函数,由不同对象调用

但是下面这种情况应该注意:

'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指向哪个对象

2.3、如果我们把 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、通过构造函数调用

3.1、通常情况:构造函数没有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.1、当构造函数包含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对象,需要注意,不过这种情况也挺少(我就从来没遇见过)。

4、通过 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 分别是 2、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 分别是 2、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));
        }
    }
}
复制代码

5、箭头函数

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一致;

谢谢大家,喜欢点个赞再走哦!!!

关注下面的标签,发现更多相似文章
评论