this 指向问题

243 阅读8分钟

this 是什么

js 中 this 指的是函数的执行主体。

我们需要注意的是,只有函数执行的时候才能确定 this 到底指向谁。

涉及 this 指向问题的场景汇总

1. 事件绑定

给某个Dom元素的某事件行为绑定函数,当事件行为被触发时,绑定的函数会执行,此时函数中的 this 一般都是当前操作的元素本身。

案例: 我们给 document 的点击事件绑定一个函数,然后用鼠标点击页面触发执行事件处理函数。代码如下:

// 1 使用 `onxxx` 方式给它绑定函数
document.onclick = function() {
    console.dir(this);  // #document
}

// 2 使用事件监听方式给它绑定函数
document.addEventListener('click', function() {
    console.log(this); // #document
});

// 3 特殊情况: IE6~8 使用 attachEvent 方式绑定事件
document.body.attachEvent('onclick', function () {
    // this -> window/undefined
});

2. 普通函数

普通函数执行时,主要是先看 . ,函数调用语句前面是否有 .

  • 如果有 . ,那么 . 前面是谁,this 就指向谁。
  • 如果没有 . ,那么 this 就指向 window / undefined

window / undefined
非严格模式下是:window
严格模式下是:undefined 后面这样写也是一个意思

普通函数执行也有几种特殊的存在:

  1. 自执行函数,执行时 this 一般指向 window / undefined 。
  2. 回调函数, 执行时 this 一般来讲都是指向 window / undefined 。但是如果在在执行回调函数时,在函数内部做了特殊处理,是可以改变 this 指向的。
  3. 在括号表达式中,只会取出最后一项来执行。如果最后一项是函数,函数执行时 this 指向 window / undefined 。如果最后一项不是函数,执行的话会报错。

案例:普通函数执行,看前面是否有 .

const fn = function fn(x, y) {
    console.log(x, y, this);
};
fn(10, 20)
//  10 20 Window ---》this 指向 window

let obj = {
    name: 'zhufeng',
    fn: fn
};
obj.fn(10, 20)
// 10 20 { name: "xiaoming", fn: ƒ }
// fn(10, 20) 函数执行前有 . ,. 前面是 obj
// ---》this 指向 obj

案例:执行同一个方法,this 指向却不同

let arr = [];
arr.push(2); //push方法中的this->arr
arr.__proto__.push(); //this->arr.__proto__
Array.prototype.push(); //this->Array.prototype

案例:回调函数中的 this

// 1. 把一个函数当作参数传给另外一个函数

const fn = function fn(callback) {
    // callback -> 传递进来的函数
    callback();  // 回调函数中的this->window
    callback.call(1); //改变了 this 指向,this->1
};

fn(function () {
    console.log(this);
});

// 2.  forEach() 方法的第2个参数,可以指定回调函数中的 this
let arr = [10, 20, 30],
    obj = {
        name: 'zhufeng'
    };

arr.forEach(function (item) {
    console.log(item, this); //window
});
arr.forEach(function (item) {
    console.log(item, this); //obj
}, obj);

案例:括号表达式 现实开发中已经很少这么用了

'use strict'

const fn = function fn(x, y) {
    console.log(x, y, this);
};
let obj = {
    name: 'zhufeng',
    fn: fn
};

(10, 20, obj.fn)();
// 最后一项  obj.fn 是函数,可以执行
// 非严格模式下输出:
// undefined undefine Window
// 严格模式下输出:
// undefined undefined undefined

// (10, obj.fn, 30)();
// Uncaught TypeError: (10, obj.fn, 30) is not a function

定时器 setTimeout 的回调函数中的this

let obj = {
    name: '小明',
    // 为对象的属性,添加一个函数类型的值
    // 这样写效果类似于:“fn:function(){}”
    //  这样写的函数没有 prototype 原型对象
    fn() {
        console.log(this);  // obj {name: "小明", fn: ƒ}
        setTimeout(function () {
            this.name = '王鹏';
            console.log(this); // Window {name: "王鹏"...}
            console.log(obj); // {name: "小明", fn: ƒ}
        }, 1000);
    }
};

obj.fn();

基于上述代码,我们想在 setTimeout 的回调函数内部,修改obj 的 name 的属性值改为 '王鹏',该怎么做呢?

// 方案1:使用变量 _this 缓存 fn() 执行时所属上下文中的 this
let obj = {
    name: '小明',
    fn() {
        console.log(this); // {name: "小明", fn: ƒ}
        let _this = this;
        setTimeout(function () {
            _this.name = '王鹏';
            console.log(this); // this->window
        }, 1000);
    }
};

obj.fn();

// 方案2:setTimeout 的回调函数,使用箭头函数形式
let obj = {
    name: '小明',
    fn() {
        console.log(this); // this -> {name: "小明", fn: ƒ}
        setTimeout(() => {
            this.name = '王鹏';
            console.log(this); // this-> {name: "王鹏", fn: ƒ}
            console.log(obj); // obj -> {name: "王鹏", fn: ƒ}
        }, 1000);
    }
};

obj.fn();

3. 构造函数「函数执行前加 new 关键字」

构造函数体中的 his 指向当前 构造函数/类 的实例。 构造函数执行与普通函数执行的核心区别在于,是否加 new 关键字执行。

4. 箭头函数(含块级上下文)

箭头函数中(含块级上下文)没有自己的 this ,用到的 this 都是它上级上下文「宿主环境」中的 this 。

箭头函数非常好用,但是不能乱用

  • 不涉及THIS,爱咋用咋用,
  • 但是一但涉及THIS问题,三四后行

案例: 箭头函数中没有自己的 this

let obj = {
    name: '小明',
    fn: () => {
        console.log(this);
    }
};
obj.fn(); //this -> window
obj.fn.call(100); //this -> window

4. 强制改变this 指向

基于Function.prototype上的call/apply/bind 三个可以强制改变函数中的 this 指向

对箭头函数使用这3个方法,也不会生效,因为他们没有自己的 this。

三者之间的区别

  1. apply() 和 call()
  • 相同点:都是立即执行调用它们的函数,并改变该函数中的 this 指向。
  • 不同点:唯一区别就是传参的方式不同。call() 是一项一项的给函数传参。apply() 要求所有参数必须以数组形式编写,但是最后的结果和一样,也是一项项传递给函数。
  1. apply() , call() vs bind()
    • apply() 和 call() 的共同特点是:会立即执行调用它们的函数。
    • bind() 是预处理处理,不会立即执行调用它们的函数,只是会预先处理 this ,把信息预先存储好而已。

案例:

有代码如下,满足下列需求:

  1. 点击 document 元素后,执行 fn 函数,并为其传递 10,20 两个参数。
  2. 并且让 fn 函数执行过程中的 this 指向 obj
const fn = function fn(x, y) {
    console.log(x, y, this);
}
let obj = {
    name: '我是obj'
}


document.onclick = fn();
// => x, y, this :  undefined, undefined, window
// 这样写的意思是把 fn 执行,
// 并把返回值赋值给 document 的点击事件


document.onclick = fn(10, 20);
// => x, y, this :  10, 20, window
// 这样写不符合需求。
// 这样写的意思是给 fn 函数传参 10 和 20 , 并把它执行
// 把返回值赋值给 document 的点击事件

document.onclick = fn;
// 这样写 document 的点击事件被触发时,才会执行 fn
// 此时 fn 中的 this 指向当前被操作元素本身,即 document,
// 浏览器还会默认给事件函数传递一个实参:事件对象 。这个实参默认由函数中的第一个形参接收。
// => x, y, this  :    MouseEvent {...}, undefined, document
// 不符合需求

document.onclick = fn.call(obj, 10, 20 );
// => x, y, this  :   10 20 {name: "我是obj", fn: ƒ}
// this 指向改变了,指向了 obj 这一点符合要求
// call() 会立即执行函数,这一点不符合需求
// 不符合需求

document.onclick = obj.fn(10, 20);
// 前提:给 obj 添加一个 fn 的属性,其属性值为 fn 函数
// 这样写符合需求
//  => x, y, this  : 10 20 {name: "我是obj", fn: ƒ}


document.onclick = function(ev) {
    // fn(); // this : window
    fn.call(obj, 10, 20); // this: obj
}
// 这样写符合需求
// document 的点击事件被触发时,执行的是匿名函数,
// 在匿名函数中基于 call() 去执行 fn ,改变this 指向 obj,并传参。
// => x, y, this :   10 20 {name: "我是obj", fn: ƒ}

document.onclick = fn.bind(obj, 10, 20 );
// 这样写符合需求
// => x, y, this :     10 20 {name: "我是obj", fn: ƒ}
// bind() 是预先处理 this。bind() 并不立即执行函数,只是把信息预先处理好。
// bind() 是执行了,其返回值是一个匿名函数,匿名函数中,已经存储了 this,传递的参数等信息。

仿写一个bind 方法

bind 方法:预先处理 this, 并不马上执行我们需要执行的函数。比如案例中为点击事件的绑定的方法。需要在点击行为发生后才执行绑定的方法,并不是马上执行函数。

使用到了 柯理化函数思想(预处理思想):函数执行产生一个闭包,预习把一些值存储起来,供其下一级上下文中使用。

const fn = function fn(x, y) {
    console.log(x, y, this);
}
let obj = {
    name: '我是obj',
    fn: fn
}
Function.prototype._bind = function _bind(context, ...params) {
    // this:  fn  context : obj  params 需要传递给 fn 函数的参数
    let self = this;
    return function(...args) {
        // args :接收匿名函数执行时,会接收到的实参比如:
        // 事件处理函数执行时,会接收到一个ev 事件对象
        // 合并参数
        params = params.concat(args)
        // 基于call,会立即执行函数,并改变其this指向
        self.call(context, ...params);
    }
}

document.onclick = fn._bind(obj, 10, 20);
// 10 20 {name: "我是obj", fn: ƒ}

仿写一个 call 方法

const fn = function fn(x, y) {
    console.log(x, y, this);
    return x+y
}
let obj = {
    name: '我是obj'
    // fn: fn
}

Function.prototype._call = function _call(context, ...params) {
    // 如果 context 接收到的是个原始值类型的值,比如: 1
    // 原始值类型的值是无法设置属性的,否则会报错
    // 假如接收到的是 null 或者 undefined, 我们把 context - > window
    // 假如接收到的是 其他原始值类型的值 , 我们把原始值变成其对应的对象类型值
    if(context == null ) context = window;
    if(!/^(object|function)$/.test(typeof context)) context = Object(context);

    // this -> fn   context -> obj    params ->需要传递给fn的参数
    // fn  和 obj 本身毫无关系
    // 要使 this -> obj,我们需要将他们关联起来
    // 如果是 obj.fn() 这样执行,this 就是 obj
    // 那我们把 fn 赋值给 obj 的某个属性即可。

    // context.fn = fn;
    // context.fn(...params); // x, y, this  ->  10 20 {name: "我是obj", fn: ƒ}
    // // 我们发现,obj 中多了一个 fn 属性,使原本的数据发生了变化,
    // // 而我们并不希望如此,所以需要把新增的 fn 属性移除
    // delete context.fn;

    // 但是假如原本的 obj 中有一个 fn 属性,如果我们这样将其移除,也会使原数据发生改变
    // 所以我们在给 obj 添加属性时,添加一个 由 Symbol 生成的唯一值作为属性名的话,不会出现这种情况了

    let key = Symbol('key'),
    result;
    context[key] = this;
    // result 接收函数的 返回值
    result = context[key](...params);
    delete context[key];

    return result;
}
fn._call(obj, 10, 20);
// 10 20 {name: "我是obj", Symbol(key): ƒ}
// 控制台展开后  Symbol(key) 属性是没有的