“ 本文正在参加「金石计划」 ”
本篇为此系列第三篇,本系列文章会在后续学习后持续更新。
第一篇:#深入学习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的几种绑定方式
一共有五种绑定方式
- 默认绑定 - 描述当没有显式绑定this时,this指向的是什么。
- 隐式绑定 - 解释如何通过调用上下文来绑定this,包括对象方法、函数嵌套和构造函数等。
- 显式绑定 - 描述如何使用apply、call和bind方法来手动绑定this。
- new绑定 - 解释当使用new关键字时如何绑定this到新创建的对象。
- 箭头函数绑定:使用箭头函数绑定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
对象。
函数中的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"
在上面的例子中,箭头函数 innerFunc
的 this
指向了它外层的执行上下文即 sayName
方法的执行上下文中的 this
,也就是对象 obj
。因此,innerFunc
函数中的 this.name
输出了对象 obj
的 name
属性。
普通函数下,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绑定方式相对应,一共三种
- call apply
- bind
- 箭头函数
- 在 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"
箭头函数
- 使用箭头函数定义函数,使其
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.如果argArray
是null
或undefined
, 则返回提供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指向的核心,其他的都是作为判断条件或者边界条件。
需要注意的有几点:
- 增加的这个属性名称必须是唯一的(时间戳,symbol,random生成)
- call需要确定参数,具体方法就是上述的generateFunctionCode函数
- 需要判断边界情况。第一个参数的值,做出对应的判断。
这三个问题一解决,那就构成了一个完整的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);
上面这个改写为了简单容易看,所以没有做条件的判断,这样更好理解,详细看apply实现的第一个版本
bind
bind在改变this指向中有几个特点
- 可以根据传入的第一个参数改变this指向,之后的参数作为this改变后函数的参数
- 传参可以在绑定bind的时候传,也能在放回的函数中传
- 返回一个函数,
- bind返回的函数作为构造函数时,this失效,但是传入的参数有效
- 调用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陷阱
- 全局
this
:在全局环境下使用this
可能导致代码执行出现意外结果,因为全局环境中this
的值是全局对象(如浏览器中的window
对象),而不是某个特定对象。在严格模式下,全局this
的值为undefined
。 - 回调函数中的
this
:如果将一个对象方法作为回调函数传递给另一个函数,那么在回调函数中使用this
可能会导致this
的值发生意外变化,从而导致错误或未定义行为。
const obj = {
name: 'Alice',
sayName() {
console.log(this.name);
},
doSomething(callback) {
callback();
}
};
obj.doSomething(obj.sayName); // 输出 undefined
在上面的代码中,因为回调函数 sayName()
是作为普通函数调用的,并没有绑定到 obj
上,所以在回调函数中使用 this
的值为全局对象。
- 构造函数中的
this
:如果在构造函数中忘记使用new
运算符创建新对象,或者在构造函数内部手动返回了一个对象,那么this
的值可能会被意外改变,从而导致错误。
复制代码
function Person(name) {
this.name = name;
}
const person = Person('Alice'); // 错误,person 的值为 undefined
// 因为没有使用 `new` 运算符,所以构造函数 `Person` 的 `this` 值指向全局对象。
- 使用箭头函数时,由于箭头函数的
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到具体的指向,可以使用三种方法
- 使用箭头函数:将回调函数定义为箭头函数,以确保它们的
this
值始终指向当前作用域中的this
。
class MyClass {
constructor() {
this.name = 'Alice';
}
async myMethod() {
await myAsyncFunction().then(() => {
console.log(this.name); // 输出 "Alice"
});
}
}
- 使用 bind() 方法:使用
bind()
方法将回调函数绑定到正确的this
上,这样就可以确保在回调函数中使用this
时不会出现意外错误。
class MyClass {
constructor() {
this.name = 'Alice';
}
async myMethod() {
await myAsyncFunction().then(function() {
console.log(this.name);
}.bind(this));
}
}
- 使用类方法:将回调函数定义为类方法之一,以确保它们会被绑定到当前实例对象上的
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基础。希望自己能在这条路上越走越好。
参考二:# JavaScript深入之从ECMAScript规范解读this
参考三:# 不使用调用和应用方法模拟实现 ES5 的绑定方法