深入学习JavaScript系列(三)——this

1,528 阅读12分钟

“ 本文正在参加「金石计划」 ”

本篇为此系列第三篇,本系列文章会在后续学习后持续更新。

 第一篇:#深入学习JavaScript系列(一)—— ES6中的JS执行上下文

第二篇:# 深入学习JavaScript系列(二)——作用域和作用域链

第三篇:# 深入学习JavaScript系列(三)——this

第四篇:# 深入学习JavaScript系列(四)——JS闭包

第五篇:# 深入学习JavaScript系列(五)——原型/原型链

第六篇: # 深入学习JavaScript系列(六)——对象/继承

第七篇:# 深入学习JavaScript系列(七)——Promise async/await generator

前言:在最开始学习的时候,总是记住了这句话:谁调用this,this就指向谁,但是在开发中遇到一些问题,this指向和我理解的不太一样,想深入透彻的理解一下this,于是便写下了这篇文章。

文章很长,是作为自己的学习笔记所以尽可能的把this的每一部分都详细的写下来,需要看具体某个知识点的同学可以跳转到对应目录查看

一 概念

ECMAScript规范中这样写:

this 关键字执行为当前执行环境的 ThisBinding。

MDN上这样写:

在大多数情况下,this 的值由函数的调用方式决定。
在绝大部分情况下,函数的调用方式决定了 this 的值。

我看了很多文章找到一个比较好的说法:this 是一个关键字,代表当前函数执行的上下文对象

然而this的值不是固定的,取决于函数的调用方式(这句话我们暂且认为是对的,接着往下看)

二 this的几种绑定方式

一共有五种绑定方式

  1. 默认绑定 - 描述当没有显式绑定this时,this指向的是什么。
  2. 隐式绑定 - 解释如何通过调用上下文来绑定this,包括对象方法、函数嵌套和构造函数等。
  3. 显式绑定 - 描述如何使用apply、call和bind方法来手动绑定this。
  4. new绑定 - 解释当使用new关键字时如何绑定this到新创建的对象。
  5. 箭头函数绑定:使用箭头函数绑定this指向.

隐式绑定

当函数作为对象的方法被调用时,this指向该对象

const obj = {
  name: 'Alice',
  sayName() {
    console.log(this.name);
  }
};

obj.sayName(); // 输出 "Alice"

显示绑定

明确了使用call 和apply来绑定this指向。

function sayName() {
  console.log(this.name);
}

const obj1 = {name: 'Alice'};
const obj2 = {name: 'Bob'};

sayName.call(obj1); // 输出 "Alice"
sayName.apply(obj2); // 输出 "Bob"

new绑定: 使用new 运算符生成构造函数时,this指向新创建的对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person = new Person('Alice', 20);
console.log(person.name); // 输出 "Alice"

箭头函数绑定

箭头函数中的this指向,始终指向箭头函数的上下文 就和调用方式无关。

const obj = {
  name: 'Alice',
  sayName() {
    const innerFunc = () => {
      console.log(this.name);
    };
    innerFunc();
  }
};

obj.sayName(); // 输出 "Alice"

二 this指向

首先先牢记一个结论this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建的时候调用的。同时。在函数执行的过程中,this一旦被确定,那就不能更改了

全局环境中的this

1、严格模式下,全局环境中的this指向undefined,并非全局对象

'use strict';

console.log(this === undefined); // 输出 true

2、非严格模式下。全局环境中的this指向全局对象,浏览器环境下是window,在 Node.js 环境中,全局对象是 global 对象。

image.png

函数中的this 按照绑定方式稍微总结了一下:

当函数作为方法调用时,this指向调用该方法的函数

当函数作为函数调用时,this指向全局对象;

当函数被使用new运算符调用时,this指向新创建的对象

当箭头函数被调用时,this指向箭头函数的执行上下文

箭头函数下,this的指向固定为箭头函数的上下文,也就是箭头函数外层的执行上下文中的this,不会根据函数的调用方式决定

// sayName做为方法被调用 this指向obj
const obj = {
  name: 'Alice',
  sayName() {
    const innerFunc = () => {
      console.log(this.name);
    };
    innerFunc();
  }
};

obj.sayName(); // 输出 "Alice"

在上面的例子中,箭头函数 innerFuncthis 指向了它外层的执行上下文即 sayName 方法的执行上下文中的 this,也就是对象 obj。因此,innerFunc 函数中的 this.name 输出了对象 objname 属性。

普通函数下,this由调用者提供,所以由调用函数方式来决定,如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象,如果函数独立调用,那么该函数内部的this,指向undefined,其实这两句话重点看前一句就行,后一句所谓的独立调用,实际上是被window调用,如果全局环境中没有该this属性,则指向undefined,

关键点,看调用者函数,也就是前一个

const obj = {
  name: 'Alice',
  sayName() {
    function innerFunc() {
      console.log(this.name);
    };
    innerFunc();
  }
};

obj.sayName(); // 输出 undefined 这是方法调用

在上面的例子中, 调用者函数是obj obj属于全局环境下的,全局环境中没有name属性,所以输出undefined。

构造函数调用:

function Person(name) {
  this.name = name;
}

const person = new Person('Alice');
console.log(person.name); // 输出 "Alice"

四 this指向的改变

改变this指向方法和上面的this绑定方式相对应,一共三种

  1. call apply
  2. bind
  3. 箭头函数
  4. 在 React 等前端框架中,使用 class fields 语法或 arrow function 来声明事件处理程序,以确保它们的 this 值指向组件实例。(不做讨论)

call apply

使用 call() 或 apply() 方法显式地指定 this 的值。 这是老生常谈的方法了,就不详细的说,具体怎么实现看第五章的模拟实现js的call apply。

function sayName() {
  console.log(this.name);
}

const obj1 = {name: 'Alice'};
const obj2 = {name: 'Bob'};

sayName.call(obj1); // 输出 "Alice"
sayName.apply(obj2); // 输出 "Bob"

bind

2、使用 bind() 方法创建一个新函数,并将 this 绑定到指定的值上

function sayName() {
  console.log(this.name);
}

const obj = {name: 'Alice'};
const boundSayName = sayName.bind(obj);

boundSayName(); // 输出 "Alice"

箭头函数

  1. 使用箭头函数定义函数,使其 this 始终指向外层执行上下文中的 this 值。
const obj = {
  name: 'Alice',
  sayName: () => {
    console.log(this.name);
  }
};

obj.sayName(); // 输出 undefined

具体的区别看下一段,在此不赘述。

五 模拟实现js的call apply bind

call apply

call和apply有很大的相同点所以放在一起写。首先先来看一下这哥俩的相同点和不同点

相同点:

call和apply的第一个参数都是用来改变this指向,严格模式下,this指向第一个参数,非严格模式下,第一个参数为null或者underfined时会自动替换为全局对象,原始值会被包装

不同点: apply只接受两个参数,第二个参数可以是数组/类数组/对象,如果后面还有参数 后面的参数忽略不计。

call接受多个参数,第二个及之后的都是传入的参数。

总结:如果参数明确,那就使用call,如果参数不明确,那就使用apply

那么我们在实现的时候,只需要实现apply,再更改参数就OK 我看了市面上大部分的手写call和apply ,最终觉得若川写的是比较详细,适合初学者。

call

详细的文章参考文末若川大佬# 面试官问:能否模拟实现JS的call和apply方法,我这里就是按照自己的理解简写了一遍,把过程都注释在代码中 参考规范是es5规范中文版

Function.prototype.apply (thisArg, argArray)
当以 thisArg 和 argArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:

不理解对应的名词到上文中去看,这里我不详细展开讲,主要是一些判断条件

1.如果 IsCallable(func)false, 则抛出一个 TypeError 异常。
2.如果 argArraynullundefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 func[[Call]] 内部方法的结果。
3.返回提供 thisArg 作为 this 值并以空参数列表调用 func[[Call]] 内部方法的结果。
4.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常。
5~8 略
9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func[[Call]] 内部方法,返回结果。
apply 方法的 length 属性是 2

 // es3中的写法 没有使用symbol 和es6语法 非常原生
// 浏览器环境 非严格模式
function getGlobalObject() {
    return this;
}
// 为了解决参数定长问题
function generateFunctionCode(argsArrayLength) {
    var code = 'return arguments[0][arguments[1]](';
    for (var i = 0; i < argsArrayLength; i++) {
        if (i > 0) {
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    // return arguments[0][arguments[1]](arg1, arg2, arg3...)
    return code;
}

Function.prototype.applyFn = function apply(thisArg, argsArray) {
    // 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
    if (typeof this !== 'function') {
        throw new TypeError(this + ' is not a function');
    }
    // 2 如果  argArray 是 null 或 undefined, 则赋值为[]
    if (typeof argsArray === 'undefined' || argsArray === null) {
        argsArray = []
    }
    // 3 如果Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 .
    if (argsArray !== new Object(argsArray)) {
        throw new TypeError('argsArray is not a object')
    }
    // 4  在外面传入的 thisArg 值会修改并成为 this 值。
    // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window
    if (typeof thisArg === 'undefined' || thisArg === null) {
        thisArg = getGlobalObject();
    }
    thisArg = new Object(thisArg);
    //设置唯一值 也可以用radom  symbol
    var _fn = '_' + new Date().getTime()
    // 万一还是有 先存储一份,删除后,再恢复该值
    var originalVal = thisArg[_fn];
    // 是否有原始值
    var hasOriginalVal = thisArg.hasOwnProperty(_fn);
    // 9.提供 `thisArg` 作为 `this` 值并以 `argList` 作为参数列表,调用 `func` 的 `[[Call]]` 内部方法,返回结果。
    // ES6版
    // var result = thisArg[__fn](...args);
    var code = generateFunctionCode(argsArray.length)
    var result = (new Function(code))(thisArg, _fn, argsArray);
    //   使用完成之后删除
    delete thisArg(_fn)
    if (hasOriginalVal) {
        thisArg[_fn] = originalVal;
    }
    return result

}

这是大佬在18年写的 那如果在es6中 应该怎么模拟呢?下面这个是我23年写的es6版本

Function.prototype.apply = function (context) {
    var context = context || window
    context.fn = this
    var result
    if (!arguments) {
        result = context.fn()
    } else {
        var args = [];
        for (var i = 0, len = arguments.elngth; i < len; i++) {
            args.push('arr[' + i + ']')
        }
        result = eval('context.fn(' + args + ')')
    }
    delete context.fn
    return result

}

上面的代码是简写之后的,具体含义呢,我放到apply中去分析,他两除了参数不同,其他逻辑是基本想通的。

apply

参考文章# JavaScript深入之call和apply的模拟实现:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

这段代码就是改变了this指向,仔细思考一下,相当于把 bar函数放到foo里面,bar.call(foo) 的结果是不是可以等于foo.bar(),最后结果都是一样的,看到冴羽大佬的这个解释我豁然开朗。

那call的指向问题,其实就是在改变指向的参数上(foo)增加被改变的函数属性(bar),使用完成之后在删除这个属性,这是this指向的核心,其他的都是作为判断条件或者边界条件。

需要注意的有几点:

  1. 增加的这个属性名称必须是唯一的(时间戳,symbol,random生成)
  2. call需要确定参数,具体方法就是上述的generateFunctionCode函数
  3. 需要判断边界情况。第一个参数的值,做出对应的判断。

这三个问题一解决,那就构成了一个完整的call函数

Function.prototype.myCall = function (context) {
    console.log(context);
    console.log(arguments);
    // 整理的context是入参中的第一个参数
    var context = context || window
    // 这句的意思是获取调用myCall的函数,并赋值给context的fn属性,this就是调用myCall的函数
    context.fn = this
    // 下一步是确定参数
    var args = []
    // arguments是入参及后面的参数组成的数组对象,具体打印如下图
    for (var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']')
    }
  //  把这个参数数组放到要执行的函数的参数里面去
    var result = eval('context.fn('+args+')')
    delete context.fn
    return result
}


var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.myCall(obj, 'pc', 18); 

image.png 上面这个改写为了简单容易看,所以没有做条件的判断,这样更好理解,详细看apply实现的第一个版本

bind

bind在改变this指向中有几个特点

  1. 可以根据传入的第一个参数改变this指向,之后的参数作为this改变后函数的参数
  2. 传参可以在绑定bind的时候传,也能在放回的函数中传
  3. 返回一个函数,
  4. bind返回的函数作为构造函数时,this失效,但是传入的参数有效
  5. 调用bind的必须是函数,否则得报错

根据我们分析的上一节call apply改变时,可以发现 bar.bind(foo),相当于foo.bar(),那么整体思路是和call apply一样的。 所以this指向改变这里我们call直接实现。

Function.prototype.myBind = function (context) {
    // 关于这里的this指代的是什么 已经在上文中提到 是指调用myBInde的函数
    if (typeof this !== 'function ') {
        throw new Error('use myBind is not a funciton ')
    }
    var self = this // 调用bind的函数
    // 获取bind函数从第二个参数到最后一个参数
    // 这里要解决的是特点二 参数放在两个位置传回
    var args = Array.prototype.slice.call(arguments, 1)
    // 返回一个函数这里,需要通过修改函数的原型来实现
    var fNOP = function () { }
    var fbound = function () {
 // 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,
// 已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。

 // 当作为普通函数时,this 指向 window,self 指向绑定函数,
 // 此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments))
        )
    }
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
    fNOP.prototype = this.prototype
    fbound.prototype = new fNOP()
    return fbound
}

六 常见的this陷阱

  1. 全局 this:在全局环境下使用 this 可能导致代码执行出现意外结果,因为全局环境中 this 的值是全局对象(如浏览器中的 window 对象),而不是某个特定对象。在严格模式下,全局 this 的值为 undefined
  2. 回调函数中的 this:如果将一个对象方法作为回调函数传递给另一个函数,那么在回调函数中使用 this 可能会导致 this 的值发生意外变化,从而导致错误或未定义行为。
const obj = {
  name: 'Alice',
  sayName() {
    console.log(this.name);
  },
  doSomething(callback) {
    callback();
  }
};

obj.doSomething(obj.sayName); // 输出 undefined

在上面的代码中,因为回调函数 sayName() 是作为普通函数调用的,并没有绑定到 obj 上,所以在回调函数中使用 this 的值为全局对象。

  1. 构造函数中的 this:如果在构造函数中忘记使用 new 运算符创建新对象,或者在构造函数内部手动返回了一个对象,那么 this 的值可能会被意外改变,从而导致错误。
复制代码
function Person(name) {
  this.name = name;
}

const person = Person('Alice'); // 错误,person 的值为 undefined
// 因为没有使用 `new` 运算符,所以构造函数 `Person` 的 `this` 值指向全局对象。
  1. 使用箭头函数时,由于箭头函数的 this 始终指向外层执行上下文的 this 值,因此它无法绑定到其他对象上,容易导致代码出错。
复制代码
const obj = {
  name: 'Alice',
  sayName: () => {
    console.log(this.name);
  }
};

obj.sayName(); // 输出 undefined

在上面的例子中,因为箭头函数 sayName()this 值指向外层执行上下文的 this 值,也就是全局对象,所以在函数中使用 this.name 的值为 undefined

七 处理Promise中的this

之前在学习promise时 也遇到了peomise中的this指向问题,所以在写这篇文章的时候我也查阅了一些资料,有的地方理解不到位,先写下来后续学习继续补充

如果在promise中使用this且想固定this到具体的指向,可以使用三种方法

  1. 使用箭头函数:将回调函数定义为箭头函数,以确保它们的 this 值始终指向当前作用域中的 this
class MyClass {
  constructor() {
    this.name = 'Alice';
  }

  async myMethod() {
    await myAsyncFunction().then(() => {
      console.log(this.name); // 输出 "Alice"
    });
  }
}
  1. 使用 bind() 方法:使用 bind() 方法将回调函数绑定到正确的 this 上,这样就可以确保在回调函数中使用 this 时不会出现意外错误。
class MyClass {
  constructor() {
    this.name = 'Alice';
  }

  async myMethod() {
    await myAsyncFunction().then(function() {
      console.log(this.name);
    }.bind(this));
  }
}
  1. 使用类方法:将回调函数定义为类方法之一,以确保它们会被绑定到当前实例对象上的 this
class MyClass {
  constructor() {
    this.name = 'Alice';
  }

  async myMethod() {
    await myAsyncFunction().then(this.myCallback.bind(this));
  }

  myCallback() {
    console.log(this.name); // 输出 "Alice"
  }
}

八 常见this代码题

题目一:

const obj = {
  name: 'Alice',
  sayName() {
    console.log(this.name);
  }
};

const fn = obj.sayName;
fn();
// undefined

解释: obj.sayName 赋值给了fn,在最后调用fn时是作为函数调用的,也就是window.fn 。所以this指代的是全局环境

题目二:

function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log(this.name);
};

const person1 = new Person('Alice');
const person2 = {name: 'Bob'};

person1.sayName.call(person2);
// Bob

解释:先看重点 最后调用方式采用了call的方式改变this指向到preson2;创建了两个对象proson1和proson2。最后改变this执行 所以输出的是Bob

题目三:

const obj = {
  name: 'Alice',
  sayName() {
    console.log(this.name);
  }
};

const fn = obj.sayName.bind({name: 'Bob'});
fn.call(obj);
// Bob

解释:使用bind改变this指向后不能二次改变

题目四:

const obj = {
  name: 'Alice',
  sayName() {
    console.log(this.name);
  }
};

setTimeout(obj.sayName, 1000);
// undefined
// 不使用定时器 obj.sayName()  返回结果为Alice
// 如果不使用定时器 做打印 console.log(obj.sayName()); 最后返回的是undefined

解释: 这个例子也很有意思,定时器内调用,由于此时 sayName() 方法是作为普通函数调用的,因此其中的 this 值指向全局对象(如浏览器中的 window 对象),而不是 obj 对象。因此输出结果为 undefined

obj.sayName()作为函数被调用,this指向全局对象;console.log(obj.sayName())作为方法被调用,this指向调用该方法的函数。 这里总结中有提到

题目五:

class MyClass {
  constructor(name) {
    this.name = name;
  }

  myMethod(callback) {
    callback();
  }
}

const obj = new MyClass('Alice');

obj.myMethod(() => {
  console.log(this.name);
});
// undefined

解释:创建了一个类 MyClass,其中包含一个实例方法 myMethod(),该方法接受一个回调函数作为参数,并在其中调用该回调函数。然后我们创建一个 MyClass 的实例对象 obj,并将其中的一个箭头函数作为回调函数传递给 myMethod() 方法。由于箭头函数的 this 值始终指向外层执行上下文的 this 值,所以在回调函数中使用 this.name 时,其值为 undefined

总结

this的学习主要包括确定this的指向,以及this指向的改变

重点:this的指向,是在函数被调用的时候确定的

1 当函数作为方法调用时,this指向调用该方法的函数

2 当函数作为函数调用时,this指向全局对象;

3 当函数被使用new运算符调用时,this指向新创建的对象;

4 当箭头函数被调用时,this指向箭头函数的执行上下文。

12 是比较重要的 不容易区分开,所以要重点记忆

其中全局对象调用this时,如果是非严格模式下。浏览器环境this指向window,node环境下this指向global

this指向的改变包括:箭头函数,new构造函数,call apply bind。

行文至此,关于this的知识点,在学习的过程中发现越深入所知甚少,还有很多点需要继续去学习,一篇文章也没办法把所有有关联的this知识点写下来。

学习到现在已经是深夜,最后唠叨几句,学习前端快两年了,一直都很浮躁,学习新知识很多时候都是去背面试题,但是背了又忘记,所以今年打算自己写一系列的文章,深入学习一下js基础。希望自己能在这条路上越走越好。

参考一:# 前端基础进阶(七):全方位解读this

参考二:# JavaScript深入之从ECMAScript规范解读this

参考三:# JavaScript核心法

参考三:# 不使用调用和应用方法模拟实现 ES5 的绑定方法

参考四:# 面试官问:能否模拟实现JS的bind方法

参考五:# 回味JS基础:call apply 与 bind

参考六:# 面试官问:能否模拟实现JS的call和apply方法