this关键字是JS中非常重要的一部分,也是JS编程基础中的基础。很多人去学习它的时候,往往会独立的去探寻this到底指向什么?但我认为这样的理解是孤立和断层的,我们不会是因为用this而用this。this是因为可以方便的解决问题而出现。同时,它的出现又能在其他方面延伸出一些“特殊的应用”。
我对于this的学习也是一个探索的过程,有些地方说的未必准确,如果大佬们发现有不对的地方,欢迎在评论区积极提出来~
this的出现有什么意义?
看下面的代码不难发现,this帮助我们引用了合适的上下文对象。
let person = {
name: "meng",
};
function read(...params) {
console.log(`${this.name} is reading`, params); // meng is reading (2) ["参数1", "参数2"]
}
read.call(person, "参数1", "参数2");
如果没有this,那这坨代码的风格就变了。看下面的代码:
let person = {
name: "meng",
};
function read(person, ...params) {
console.log(`${person.name} is reading`, params); // meng is reading (2) ["参数1", "参数2"]
}
read(person, "参数1", "参数2");
1. 上下文对象和参数混在了一起
我们无法第一眼看出来person的作用,也许他是函数的上下文对象,或许他只是一个普通参数,代码可读性不高
2. 可维护性差
如果我们的对象名变更了,不叫person而是叫student,一下子就要改很多地方。这只是短短几行代码,如果现在有500多行代码,肯定蒙圈了。
所以this在我们开发过程中是十分重要的,但是在使用this的过程中,我相信每个人在小白时期都会有不解的时候。
- 明明在对象中写了变量,为什么会
this.xx = undefined
- 看到同事经常用
let that = this; that.xxx();
,为什么要这样写?
下面来看下,this到底指向什么?
this的四种绑定规则
学习this首先要明白,this是函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。所以它与this所在的作用域无关。
简单点说,就是和调用的地方有关系,和定义的地方没关系。
4种绑定规则
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
优先级是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
注:箭头函数比较特殊,不受限于这4种规则,取决于this的作用域
默认绑定
不带任何修饰的函数直接调用,就是默认绑定。默认绑定的this绑定到window
var a = 2;
function foo() {
console.log(this.a);
}
// 直接调用(默认绑定)
foo(); // 2
严格模式下this是undefined
this的绑定规则取决于调用位置,这句话的前提是foo()运行在非严格模式下,也就是说非严格模式下默认绑定能绑定到全局对象。严格模式下不可以绑定this,this就是undefined
"use strict";
var a = 2;
function foo() {
console.log(this.a); // 报错
}
foo();
var a = 2;
function foo() {
"use strict";
console.log(this.a); // 报错
}
foo();
但需要注意的是,严格模式指的是函数体是否处于严格模式,而不是调用位置是否处于严格模式。如果函数体处于严格模式,this会被绑定为undefined,否则this会被绑定到全局对象。
var a = 2;
function foo() {
console.log(this.a); // 2
}
(function () {
"use strict";
foo();
})();
隐式绑定
隐式绑定是通过对象属性调用函数,并且绑定的是最后一层调用,看下面这个例子。
obj1.obj2.foo(); // foo中的this绑定的就是obj2
隐式丢失
this绑定时,被隐式绑定的函数有时会丢失绑定对象,这时候会应用默认绑定,把this绑定在全局对象或者undefined上(取决于是否为严格模式)。
以下这几种情况会引起隐式丢失:
- 函数别名
- 传入回调函数
函数别名
函数别名就是用一个变量名去接收一个引用,如下。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
var bar = obj.foo;
bar(); // undefined
在上面代码中,bar是obj.foo的一个引用,但它引用的是foo本身,因此bar是默认绑定。
传入回调函数(自定义函数)
用回调函数作为参数,回调函数本身会隐式丢失,如下
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo,
};
var a = "global";
doFoo(obj.foo); // global
传入回调函数(内置函数)
以上情况是把参数传到自己定义的函数中,如果把函数传到内置函数中,同样会隐式丢失,这是我们日常开发中十分常见的一种形式。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
var a = "global";
setTimeout(obj.foo); // global
setTimeout(function () {
console.log(this); // window
});
// setTimeout 相当于一个普通函数
function setTimeout(fn) {
fn();
}
显式绑定(硬绑定)
apply、call、bind
显式绑定采用的是call,apply,强行定义函数中的this。call和apply二者的区别仅在于传参的形式不同。
function foo(...params) {
console.log(this, params); // obj ["参数1", "参数2"]
}
let obj = {
a: 2,
};
foo.call(obj, "参数1", "参数2");
foo.apply(obj, ["参数1", "参数2"]);
let a = foo.bind(obj);
a("参数1", "参数2");
this为null
apply,call绑定还有一种特殊情况,就是this是一个null,那么会应用默认绑定。
function foo() {
console.log(this.a);
}
var a = 2;
foo.apply(null); // 2
在实现函数柯理化的时候,会用到xx.apply(null)
的形式来接收参数,具体实现函数柯理化的过程在文末有写。
注意事项:
- 这个操作会在window上挂载一个a属性,但这种绑定形式会修改全局对象,从而造成一些难以分析的bug。
- 可使用 Object.create 来创建一个空对象,这样既不影响应用,又不会污染全局变量
let _null = Object.create(null);
function foo() {
console.log(this.a);
}
var a = 2;
foo.apply(_null); // undefined
可选参数作为上下文
很多内置函数也给我们提供了一个可选参数作为上下文,比如forEach,这也是显式绑定的一种形式。
let obj = {
a: 2,
};
["参数1", "参数2"].forEach(function (item) {
console.log(item, this.a); // 参数1 2
}, obj);
注意:当我们使用箭头函数时,这种显示绑定就不生效了
["参数1", "参数2"].forEach((item) => {
console.log(item, this.a); // 参数1 undefined
}, obj);
硬绑定
显式绑定也称为硬绑定,因为一旦显式绑定后,无法第二次再通过其它绑定来修改this的指向(new绑定可修改,但这属于特殊情况)。后面的要说的软绑定解决了这个问题。
var a = 3;
function foo() {
console.log(this.a);
return this.a;
}
let obj = {
a: 2,
};
let bar = function () {
foo.call(obj);
};
bar(); // 2
bar.call(window); //2,this没有被修改为window
new绑定
new绑定是构造出一个新对象,把新对象绑定在this上。任何函数都可以通过new来调用,new的这种调用形式被称为构造函数调用。
function foo(a) {
this.a = a;
}
// 把新对象绑定在this上
let bar = new foo(2);
console.log(bar.a); //2
再来看一个复杂点的例子
function foo(something) {
this.a = something;
}
let obj1 = {
foo,
};
let obj2 = {};
obj1.foo(2);
let bar = new obj1.foo(4);
console.log(obj1.a); // 2
console.log(bar.a); //4
总结一下:使用new来调用函数时,会执行下面的操作
- 创建一个全新的对象
- 新对象执行prototype连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数会自动返回新对象
对于第四个流程中的,函数返回新对象,我们可以测试下
// 函数没有返回其他对象,new表达式返回新对象
function foo(a) {
this.a = a;
}
let bar = new foo(2);
console.log(bar); // foo {a: 2}
// 函数返回了其他对象,new表达式返回函数返回的对象
function foo(a) {
this.a = a;
return {
b: this.a,
c: 1,
};
}
let bar = new foo(2);
console.log(bar); // {b: 2, c: 1}
// 函数返回的不是一个对象的时候,new表达式会忽略这个返回,返回的还是默认的对象
function foo(a) {
this.a = a;
return "测试";
}
let bar = new foo(2);
console.log(bar); // foo {a: 2}
new绑定修改了硬绑定的this
在下面的代码中,通过显式绑定,将this指向obj1,obj1.a = 3。 new绑定通过创建一个新对象的方式,修改了this的指向,使baz.a = 3,所以new是唯一可以修改硬绑定的方式。
function foo(something) {
this.a = something;
}
var obj1 = {};
let bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
在new绑定中使用硬绑定,方便了参数的传递,也是柯理化的一种形式。
function foo(p1, p2) {
this.val = p1 + p2;
}
let bar = foo.bind(null, "p1");
var baz = new bar("p2");
console.log(baz.val); // p1p2
特殊的箭头函数
四条绑定规则可以适用于所有函数,但箭头函数除外。箭头并不适用于上面的规则,箭头函数中this的指向,根据它所处的作用域来决定。
function foo() {
return (a) => {
console.log(this.a);
};
}
let obj1 = { a: 2 };
let obj2 = { a: 3 };
let bar = foo.call(obj1);
bar.call(obj2); // 2
显式绑定bar.call不能改变箭头函数的指向,箭头函数中的this继承自它的作用域,foo中的this指代的是obj1。
箭头函数常用于回调函数中,比如定时器中。因为这种情况会出现隐式丢失的情况,使用箭头函数可以绑定外层的this。
function foo() {
setTimeout(() => {
// 这里的this,指带的是foo
console.log(this.a);
});
}
let obj = { a: 2 };
foo.call(obj); // 2
练习题
前面我们学习完了各种形式,现在可以做几道题检验一下。
题1
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
答案是undefined,foo是一个默认绑定。
题2
function foo(num) {
this.count++;
}
foo.count = 0;
for (let i = 0; i < 5; ++i) {
foo(i);
}
console.log(foo.count);
答案是0,调用foo的时候是默认绑定,默认绑定this绑定到了window上,相当于在全局创建了一个count。但window.count是undefined,undefined + 数字 = NaN
window.count; // NaN
题3
let count = 0;
window.count =0;
function foo(num) {
this.count++;
}
foo.count = 0;
for (let i = 0; i < 5; ++i) {
foo(i);
}
console.log(count);
console.log(window.count);
答案是0和5。
其他
非严格模式下,var声明的全局变量都会成为window的属性,而let,const 声明的变量不会绑定给window对象。
我自己平时也会刷一些题目来巩固自己的理解,这里推荐一篇文章,他总结的题还是挺全面的。
手撕代码
本文涉及到了函数柯理化,以及apply,call,bind,顺道我们就看下他们具体的内部实现是什么样的
函数柯理化
函数柯理化就是将接收多个参数的函数,变为可以接收单个参数的函数。
add(1, 2, 3, 4);
柯理化后
add(1)(2)(3)(4)
实现的思路:
- 比较传入参数的个数,是否等于函数的参数个数
- 如果相等则返回计算结果,如果不等则继续返回接受参数后的新函数
let _null = Object.create(null);
const curry = (fn, ...args) =>
fn.length > args.length
? (...params) => curry(fn, ...args, ...params)
: fn.apply(_null, args);
let addSum = (a, b, c, d) => a + b + c + d;
let add = curry(addSum);
console.log(add(1)(2)(3)(4));
console.log(add(1, 2, 3, 4));
console.log(add(1, 2, 3, 4, 5));
结尾
以上就是所有东西啦,本文是读了《你不知道的javascript》写出的文章,理解后做了自己的总结,推荐大家可以自己去看下这本书。有些东西理解的还比较浅显,或者不够全面。欢迎大家在评论区提出宝贵意见。
下期再见。