JS基础 - 判断 this 的指向

this 是继承自父级的执行上下文,且 JS 中 this 的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定 this 到底指向谁(箭头函数除外),实际上 this 的最终指向的是那个调用它的对象

直接调用

直接调用是指通过函数名()的方式调用。

function f(){
    let a = 'A';
    console.log(this.a); // undefined
    console.log(this); // Window
}
f();

这里的 fn() 实际上是在全局对象上调用,即 Window.fn() ,所以 this 指向的是 Window

将例子复杂化:

const f = {
  name : " A ", 
  showThis: function(){
    console.log(this);  // Object f
    function bar(){ console.log(this) };  // Window
    bar();  // 此处相当于 Window.bar()
  }
}
f.showThis(); 

方法调用

方法调用是指通过对象来调用其方法函数。

const o = {
    a: 'A',
    b: function(){
        console.log(this.a); 
    }
}
o.b(); // A

这里是 o 在调用 b() ,所以 this 指向的是 o ,所以就取到了 A 。

如果是多层嵌套会是什么样呢?

结论:不论嵌套了多少层,都指向最近的调用函数的对象,看个例子:

const o = {
    a: 'A',
    b: {
        a: 'A2',
        c:function(){
            console.log(this.a); 
        }
    }
}
o.b.c(); // A2

最近的调用函数的对象内没有需要的变量 a,那么会会逐级向上查找吗?

结论:会返回 undefined,this 的指向只会指向他的上一级对象,即使上上一级对象内有需要的东西。

将上面的例子改一下,删除 b 内部的 a:

const o = {
    a: 'A',
    b: {
        c:function(){
            console.log(this.a); 
        }
    }
}
o.b.c(); // undefined

此外还有一种特殊情况,是直接调用和方法调用的混合:

const o = {
    a: 'A',
    b: {
        a: 'A2',
        c:function(){
            console.log(this.a); 
        }
    }
}
const f = o.b.c;
f(); // undefined

看起来是方法调用,但最后调用 fn() 时是直接调用,Window 中没有定义 a ,所以返回 undefined。

立即执行函数

立即执行函数,立即执行函数不管是在全局定义还是在函数内定义,this 都指向 Window。

Window.a = 5;
(function(){ console.log(this.a) })(); // 5,此时 this === Window

箭头函数

箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。且这个指向在定义的时候就已经确定了,并不会在调用时指向其执行环境的对象,所以 call、apply 和 bind 方法并不能对箭头函数起作用。

var x=11;
let obj={
 x:22,
 say:()=>{
   console.log(this.x);
 }
}
obj.say(); // 11

上述代码中,箭头函数里面的 this 向外寻找外层函数,但 obj 不是个函数也没有外层函数包裹,所以 this 最终指向了 Window。

需要注意的是:如果这里的 var 替换成 let,则打印出的是 undefined,因为 var 和 function 声明的全局变量是顶级对象 window 的属性,而 let / const 声明的全局变量不属于顶级对象的属性了。

如果将上面代码改造一下,在外层包裹一个自执行函数,此时 this 就指向了自执行函数:

var x=11;
(function(){
    this.x = 33;
    let obj={
        x:22,
        say:()=>{
            console.log(this.x);
        }
    }
    obj.say(); // 33
})()

构造函数

构造函数需要使用 new 关键字构建实例,通常会经历以下 4 个步骤:

  1. 创建一个对象:let newObj = {};
  2. 将构造函数的原型赋值给新对象:Object.setPrototypeOf( newObject , foo.prototype);
  3. 更改构造函数 this 指向新对象,然后执行构造函数的代码:foo.call( newObj );
  4. 返回新对象;

所以使用 new 运算符构建实例时,this 会指向新生成的对象:

function f(){
    this.a = 'A';
}
const b = new f();
console.log( b.a ) // A

在构造函数中,return 会影响 this 的指向:当 return 回来的值是对象时,那么 this 就会指向这个返回的对象,如果不是对象,则会指向原函数实例。

function f(){
    this.a = 'A';
    return {}
}
const b = new f();
console.log( b.a ) // undefined
function f(){
    this.a = 'A';
    return 1
}
const b = new f();
console.log( b.a ) // A

再来看一下类内部函数在外部调用时的场景:

class Demo {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
    sum() {
        let sumVal = this.x + this.y;
        return sumVal;
    }
}

let myDemo = new Demo(2, 3);
const sum = myDemo.sum; // 将sum的引用赋值给新变量sum
sum();// Uncaught TypeError: Cannot read properties of undefined (reading 'x')

此时的 this 还是会按照方法调用的规则,指向运行时所在的环境(Window),所以找不到对应的 x 值。

那么,如何实现 this 指向实例呢?

可以在类的构造函数中,显式绑定 this:

class Demo {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.sum = this.sum.bind(this); // sum中this显式绑定
    }
    sum() {
        let sumVal = this.x + this.y;
        return sumVal;
    }
}

let myDemo = new Demo(2, 3);
const sum = myDemo.sum; 
sum(); // 5

或者使用箭头函数来申明函数:

class Demo {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    sum = () => {
        let sumVal = this.x + this.y;
        return sumVal;
    }
}

let myDemo = new Demo(2, 3);
const sum = myDemo.sum; 
sum(); // 5

修改 this 指向

改变 this 指向主要有三种方法:bind、call、apply ,其中 bind 对 this 指向所造成的影响最为深远,慎用。

bind 将当前函数和对象绑定,并返回一个新的函数(注意是返回一个新函数而不是修改原函数的 this 指向),不论新函数以何种方式调用,this 始终指向绑定的对象。

const o = {};
function a(){
    console.log(this === o);
}
const b = a.bind(o);
a(); // false
b(); // true

使用了 bind 后,使用 apply 或者 call 都无法再改变 this 的指向。

const o = {};
function a(){
    console.log(this === o);
}
const b = a.bind(o);
b.apply({})
b(); // true

call 和 apply 的区别:传参的方式不同,call 接收若干个参数列表,而 apply 接收一个由多个参数组成的数组

func.call( thisArg , arg1 , arg2 ,… )

const foo = function (sex,address) {
    console.log(this.name);
    console.log(sex);
    console.log(address);
}
const Lucy = {
    name: 'lucy',
}
foo.call(Lucy, 'male', 'hangzhou'); // Lucy,male,hangzhou

func.apply( thisArg , [ arg1 , arg2 ,… ] )

const foo = function (sex,address) {
    console.log(this.name);
    console.log(sex);
    console.log(address);
}
const Lucy = {
    name: 'lucy',
}
foo.apply(Lucy, ['male', 'hangzhou']); // Lucy,male,hangzhou

需要注意的一点是:在非严格模式下,如果传入的第一个参数为 undefined 或 null 或不传,那么 this 会默认指向 Window 对象,而在严格模式下,this 严格指向传入的第一个参数。

手写一个 call 方法

承接上一个例子

const foo = function (sex,address) {
    console.log(this.name);
    console.log(sex);
    console.log(address);
}
const Lucy = {
    name: 'lucy',
}

考虑使用 call 方法,foo 中的 this 指向调用他的对象,所以如果 foo 要获取到 this.name,就需要实现Lucy.foo()这样的调用方式,那么 foo 应该变为 Lucy 的一个属性,如下所示:

const Lucy = {
    name: 'lucy',
    foo:function (sex,address) {
        console.log(this.name);
        console.log(sex);
        console.log(address);
    }
}

所以 call 方法的本质就是,给 Lucy 增加一个临时属性,并在调用后删除该属性。

首先在 Function 原型链上定义一个 call2 的属性:

Function.prototype.call2 = function(target,...args){}

这里不能使用箭头函数,如果是箭头函数,那么函数中的 this 就指向了全局的 window 对象,而不是 foo。

因为在调用的时候是foo.call(obj)的形式,所以 call 方法内 this 指向的是 foo 函数,这样一来就获取到了目标函数。

然后创建一个永远都不会重复的唯一键(用 Symbol 实现),foo 函数作为值:

Function.prototype.call2 = function(target,...args){
    const uniqueKey = Symbol();
    target[uniqueKey] = this; // this 是 foo 函数
    target[uniqueKey](...args); // 等同于 Lucy.foo(...args)
    delete target[uniqueKey]; // 删除创建的临时属性
}

这样一个简易的 call 方法就完成了。

测试用例:

const foo = function (sex,address) {
    console.log(this.name);
    console.log(sex);
    console.log(address);
}

const Lucy = {
    name: 'lucy',
}

Function.prototype.call2 = function(target,...args){
    const uniqueKey = Symbol();
    target[uniqueKey] = this; 
    target[uniqueKey](...args); 
    delete target[uniqueKey];
}

foo.call2(Lucy,'male','hangzhou') // lucy male hangzhou

手写一个 bind 方法

bind 方法需要通过 call 方法实现,本质就是返回一个函数,然后在函数内执行 call 方法改变 this 指向。

由于返回了一个函数,在调用的时候产生了闭包,保持了对 this 的引用,解释了为什么使用 bind 后 call 和 apply 都无法再改变 this 指向。

Function.prototype.bind2 = function(target,...args){
    const self = this;
    return function(){
        self.call( target , ...args )
    }
}

测试用例:

const foo = function (sex,address) {
    console.log(this.name);
    console.log(sex);
    console.log(address);
}

const Lucy = {
    name: 'lucy',
}

Function.prototype.bind2 = function(target,...args){
    const self = this;
    return function(){
        self.call( target , ...args )
    }
}

foo.bind2(Lucy,'male','hangzhou')() // lucy male hangzhou

JS基础 - 判断 this 的指向

https://hashencode.github.io/post/671ffc39/

作者

BiteByte

发布于

2020-08-08

更新于

2024-11-15

许可协议