# 理解this
在掌握 this 之前,我们需要知道下面这些点
- this 不指向函数本身
- this 是在函数被调用时发生的绑定,也就是说,函数未调用之前是没有 this 的,它指向谁完全取决于函数在哪里被调用
- 每一个函数被调用时,都会创建一个执行环境(上下文环境),执行环境中的活动对象, 含有函数在哪里调用(调用栈),函数的调用方式,传入的参数等信息,this 是这个活动对象的一个属性,会在函数的执行过程中用到,执行环境不存在,this 也就不存在了,如果没有特殊指定,活动对象的 this 是 window
# 1. this 的绑定规则
# 1.1 默认绑定
在全局作用域调用函数,函数的 this 是 window(在严格模式为 undefined)
在局部作用域直接调用函数,应用的也是默认绑定,即 window,当前函数只查找到了 this 即 window 自己
function foo() {
console.log(this); // window
}
function bar() {
console.log(this); // window
foo();
}
bar();
# 1.2 隐式绑定
当函数引用具有上下文对象时,隐式绑定规则会把 this 绑定到这个上下文对象上
即谁调用的函数,谁就是函数上下文中的 this,在下例中,是 obj
function foo() {
console.log(this); // obj对象
console.log(this.a); // 2
}
const obj = {
a: 2,
foo: foo
};
obj.foo(); // 输出为obj对象,即{a:2,foo:foo}
# 1.3 显示绑定
通过call(...)
apply(... )
bind(...)
方法
apply()
方法第一个参数是一个对象,在调用函数时将 this 绑定到这个对象上,因为直接指定 this 的绑定对象,称之为显示绑定
function foo() {
console.log(this); // obj对象
}
const obj = {
a: 2
};
foo.call(obj); // foo函数执行
apply()
方法第二个参数是一个数组,数组里面是调用函数时要传递的参数,函数执行时接收的参数是数组的一个一个项(即数组被打散了)
比如:
function foo(arg1, arg2, arg3) {
// arg1,arg2,arg3对应的分别是**参数数组**的第一项,第二项,第三项
console.log(arguments); // [1,2,{name: 'xbl'}] arguments为伪数组
console.log(arg1); // 1
console.log(arg2); // 2
console.log(arg3); // {name: 'xbl'}
}
const obj = {};
foo.apply(obj, [1, 2, { name: "xbl" }]);
foo.apply(obj, 1); // 传递非数组会报错
// 使用ES6展开运算符展开参数更方便,展开运算符结合函数参数使用
function foo(...args) {
console.log(...args); // 1 , 2, name: 'xbl'
console.log(...arguments); // 1 , 2, name: 'xbl'
console.log(args); // 数组 [1,2, name: 'xbl']
console.log(arguments); // 伪数组
}
大多数使用 apply 的场景可以使用 ES6 的**展开运算符(spread operator)**进行代替,比如:
// 从数组中取出最大值
const values = [25, 50, 75, 100];
Math.max.apply(Math, values);
// 可以使用展开运算符
Math.max(...values);
call()
方法与apply()
不同的是,可以有 2-n 个参数,第 2-n 个参数是调用函数要传的参数
function foo(arg1, arg2, arg3) {
console.log(arguments); // [1,2,{name: 'xbl'}] arguments为伪数组
console.log(arg1); // 1
console.log(arg2); // 2
console.log(arg3); // {name: 'xbl'}
}
const obj = {};
foo.call(obj, 1, 2, { name: "xbl" });
bind()
可以改变 this 指向,接收的参数也与call
类似,不过 bind 返回的是一个函数
function foo(a, b) {
console.log("a:", a, "b:", b); // a: 2 b: 3
}
// bind是高阶函数,细分的话是柯里化函数
// 函数柯里化是指 函数对传入的参数做处理,每处理一个参数,返回一个函数,是函数式编程的重要组成部分
const bar = foo.bind({}, 2, 3);
bar();
ES6 使用 apply 实现实现一个简单的 bind
Function.prototype.bind1 = function(...rest1) {
const self = this; // 这里的this是bind1的调用者(隐式绑定) 在本例中是foo。
const context = rest1.shift(); // 第一个传入参数作为this, shift可以直接改变原数组
return function(...rest2) {
// rest1即bind1接收的参数,本例中对应 2, 3。rest2即返回函数接收的参数。本例中对应 4
// 最终执行的还是原本传过来的函数,本例中是foo
// return self.apply(context, [...rest1, ...rest2]);
return self(...rest1, ...rest2) // 和上面效果一样
};
};
function foo(a, b, c) {
console.log("a:", a, "b:", b, "c:", c);
}
const bar = foo.bind1({}, 2, 3);
bar(4);
# 注意事项
把 null
或者 undefined
作为 this 绑定的对象传入 call, apply, bind
, 这些值 在调用时会被忽略,实际应用的仍然是默认规则
使用 null 可能会产生副作用,可以传空对象{}
js 中创建空对象最简单的方法时 Object.create(null), 这个和{}很像,但是不会创建Object.prototype这个委托,比{}更空,即没有原型链
function foo(a, b) {
console.log("a:", a, "b:", b); // a:2 b:3
console.log(this); // {} 空对象
}
const empty = Object.create(null);
foo.apply(empty, [2, 3]);
# 1.4.new 绑定
- js 中,构造函数只是使用 new 操作符时被调用的普通函数
- 内置对象如 Number,Object 等等在内的所有构造函数都可以用 new 调用,这种调用方式称为构造函数调用
- 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”
使用 new 调用函数时,会自动执行以下操作
- 执行构造函数
- 创建一个新对象
- 新对象的原型(proto/prototype)指向构造函数的 prototype
- 新对象赋给当前 this
- 如果函数没有返回其他对象,new 表达式中的函数会自动返回这个新对象
function Animal(name) {
this.name = name
}
Animal.prototype.say = function() {
console.log('haha')
}
// 后代实例
const dog = new Animal('jack')
console.dir(dog)
// 实例属性对象
{
name: 'jack',
__proto__: {
say: f(),;
constructor: f Animal(name) {
arguments: null,
length: 1,
name: 'Animal', ...
} ...
}
}
如何判断一个函数是被 new 操作符调用了?可以使用 es6 新增的 new.target
进行判断
const Person = function(name) {
if (typeof new.target === 'undefined') {
throw new Error('必须要通过关键字new调用构造函数')
}
this.name = name
}
new Person('xbl')
Person('xbl') // 报错
this 绑定的优先级:new > apply/call/bind > 隐式绑定 > 默认绑定
# 2.ES6 箭头函数中 this 的词法特征
- 箭头函数无法使用上述四条 this 绑定的规则
- 箭头函数内部不存在 this,但是通过作用域链,可以查找到 this 变量,如果外层作用域(函数)中有 this((非默认绑定的 this),则箭头函数 this 与外层作用域 this 一致,否则会向上查找,直到全局作用域
- 箭头函数的 this 无法被直接修改,但是可以通过改变外层函数的 this 指向来间接改变箭头函数里的 this
- 箭头函数没有构造函数 constructor, 不可以使用 new 调用
# 2.1 箭头函数示例
// 箭头函数中this的默认行为
function b1() {
function b2() {
console.log(`b2函数this`, this); // window
const b3 = () => {
console.log(`b3箭头函数this`, this); // 与b3函数父函数(外层函数b2)this一致 window
};
b3();
}
b2();
return () => {
console.log(`return箭头函数a`, this.a); // 与b1this一致
};
}
const o1 = { a: 1 };
const bar = b1.call(o1); // b1 this指向 o1, 执行b1(),返回一个箭头函数
bar(); // 箭头函数执行,与b1 this 一致,为 o1
// 测试箭头函数是否可以改变this
const o2 = { a: 2 };
const o3 = { a: 3 };
bar.call(o2); // 箭头函数无法直接使用显示绑定,输出 this 仍为 o1
// 那就只能通过改变箭头函数的 外层函数 this 从而改变箭头函数的this
const baz = b1.call(o3);
baz(); // 成功改变箭头函数this 为o3 !!
# 2.2 多个嵌套非箭头函数
function foo() {
console.log(`foo内部`, this); // outObj,显式绑定
function b1() {
console.log(`b1`, this); // innerObj,隐式绑定
function b2() {
console.log(`b2`, this); // window,默认绑定
function b3() {
console.log(`b3`, this); // window,默认绑定
}
b3();
}
b2();
}
const innerObj = { inner: 2, bar };
return innerObj.b1();
}
const outObj = { outObj: 1 };
foo.call(outObj);
可以看出,每个函数的 this 都是独立的,无法继承自父函数,默认规则绑定的是 window
如果想要保存父函数的 this,可以使用that = this
,用 that 来持续引用父函数的 this,父函数执行过后执行环境不会立即销毁,这里借用了闭包的原理。
# 2.3 对象中属性值为函数时
var a = "hello";
const obj = {
a: "world",
b: this,
f1: () => {
console.log(this.a);
},
f2: function() {
console.log(this.a);
}
};
// 对象是没有上下文环境,因此也就没有this这个属性,之所以可以获取到this是因为全局作用域存在this变量
console.log(obj.b); // windnow
obj.f1(); // hello
obj.f2(); // world
# 2.4 回调函数里面的 this
当函数的接收参数是函数时,这个函数参数的this指向是什么呢?比如
function cb() {}
function foo(cb) {
cb()
}
如果回调函数是一个普通函数,并且没用 this 绑定规则,那么 this 将会是window
function bar() {
const cb = function(callback) {
callback();
};
// 普通函数
cb(function() {
console.log(`回调普通函数`, this); // window
});
// 箭头函数
cb(() => {
console.log(`回调箭头函数`, this); // {a: 1},与bar一致
});
const innerObj = { cb: cb };
innerObj.cb(function() {
console.log(`回调普通函数`, this); // window
});
innerObj.cb(() => {
console.log(`回调箭头函数`, this);
});
// 注意!定义的位置,也就是cb的位置,this是cb外面函数的this, 与bar一致,即outObj { a: 1 };
}
const outObj = { a: 1 };
bar.call(outObj);
# 2.5 定时器
对于一些使用定时器的地方,想要保存父级的 this,可以使用箭头函数,而不用再用that = this
来用闭包保存父级 this
function foo() {
const self = this;
setTimeout(function() {
console.log(self); // obj
console.log(this); // window 默认绑定
}, 1000);
setTimeout(() => {
console.log(this); // obj,回调函数的this
}, 2000);
}
const obj = { a: 1 };
foo.call(obj); // foo this 是 obj
# 2.6 this 的一个例子
var a = 20;
// 用const a = 20 无法得到想要的输出,因为此时a没有被绑定到window对象上
const obj = {
a: 40,
// 应用默认绑定,this是window
foo: () => {
console.log(this.a); // 词法作用域,obj外面的this, this是window对象,输出20
function func() {
console.log(this) // this在下例中是window
this.a = 60; // 语法作用域
console.log(this.a); // this不确定,但是this.a确定,是60
}
func.prototype.a = 50; // func的prototype的a值是50
return func;
}
};
const bar = obj.foo(); // 执行foo函数,并返回func函数
bar(); // 执行bar函数 60
new bar(); // 60
# 2.7 箭头函数没有构造器 constructor,不能用于构造函数
const Message = text => {
this.text = text;
return this
};
const myMessageInfo = Message("hi");
console.log(myMessageInfo.text); // hi
const Message2 = text => {
this.text = text;
};
const myMessage = new Message2("hello");
// Uncaught TypeError: Message is not a constructor
因为 this 的问题,箭头函数要慎用,构造函数不能使用箭头函数,因为 prototype 无法指定