作者:Dmitri Pavlutin
译者:前端小智
来源:dmitripavlutin.
原文的篇幅非常长,不过内容太过于吸引我,还是忍不住要翻译出来。我将原文分为上下两部分,本篇是第二部分,第一部分请看这:
为了保证的可读性,本文采用意译而非直译。
5. 隐式调用
使用myFun.call()
或myFun.apply()
方法调用函数时,执行的是隐式调用。
JS中的函数是第一类对象,这意味着函数就是对象,对象的类型为Function
。从函数对象的方法列表中,.call()
和.apply()
用于调用具有可配置上下文的函数。
下面是隐式调用的例子
function increment(number) { return ++number; }increment.call(undefined, 10); // => 11increment.apply(undefined, [10]); // => 11
return ++number;
}
increment.call(undefined, 10); // => 11
increment.apply(undefined, [10]); // => 11
increment.call()
和increment.apply()
都用参数10
调用了这个自增函数。
两者的区别是.call()
接受一组参数,例如myFunction.call(thisValue, 'value1', 'value2')
。而.apply()
接受的一组参数必须是一个类似数组的对象,例如myFunction.apply(thisValue, ['value1', 'value2']
)。
5.1. 隐式调用中的this
在隐式调用.call()或.apply()中,this是第一个参数
很明显,在隐式调用中,this
作为第一个参数传递给.call()
或.apply()
。
var rabbit = { name: 'White Rabbit' };function concatName(string) { console.log(this === rabbit); // => true return string + this.name;}concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'name: 'White Rabbit' };
function concatName(string) {
console.log(this === rabbit); // => true
return string + this.name;
}
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'
当应该使用特定上下文执行函数时,隐式调用非常有用。例如为了解决方法调用时,this
总是window
或严格模式下的undefined
的上下文问题。隐式调用可以用于模拟在一个对象上调用某个方法。
function Runner(name) { console.log(this instanceof Rabbit); // => true this.name = name; }function Rabbit(name, countLegs) { console.log(this instanceof Rabbit); // => true Runner.call(this, name); this.countLegs = countLegs;}const myRabbit = new Rabbit('White Rabbit', 4);myRabbit; // { name: 'White Rabbit', countLegs: 4 }
console.log(this instanceof Rabbit); // => true
this.name = name;
}
function Rabbit(name, countLegs) {
console.log(this instanceof Rabbit); // => true
Runner.call(this, name);
this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }
Rabbit
中的Runner.call(this, name)
隐式调用了父类的函数来初始化这个对象。
6. 绑定函数
绑定函数是与对象连接的函数。通常使用.bind()
方法从原始函数创建。原始函数和绑定函数共享相同的代码和作用域,但执行时上下文不同。
方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])
接受第一个参数thisArg
作为绑定函数执行时的上下文,并且它接受一组可选的参数 arg1, arg2, ...
作为被调用函数的参数。它返回一个绑定了thisArg
的新函数。
function multiply(number) { 'use strict'; return this * number;}const double = multiply.bind(2);double(3); // => 6double(10); // => 20
'use strict';
return this * number;
}
const double = multiply.bind(2);
double(3); // => 6
double(10); // => 20
bind(2)
返回一个新的函数对象double
,double
绑定了数字2
。multiply
和double
具有相同的代码和作用域。
与.apply()
和.call()
方法相反,它不会立即调用该函数,.bind()
方法只返回一个新函数,在之后被调用,只是this已经
被提前设置好了。
6.1. 绑定函数中的this
在调用绑定函数时,
this
是.bind()
的第一个参数。
.bind()
的作用是创建一个新函数,调用该函数时,将上下文作为传递给.bind()
的第一个参数。它是一种强大的技术,使咱们可以创建一个定义了this
值的函数。
来看看,如何在如何在绑定函数设置 this
const numbers = { array: [3, 5, 10], getNumbers: function() { return this.array; }};const boundGetNumbers = numbers.getNumbers.bind(numbers);boundGetNumbers(); // => [3, 5, 10]// Extract method from objectconst simpleGetNumbers = numbers.getNumbers;simpleGetNumbers(); // => undefined (严格模式下报错)
array: [3, 5, 10],
getNumbers: function() {
return this.array;
}
};
const boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined (严格模式下报错)
numbers.getNumbers.bind(numbers)
返回绑定numbers
对象boundGetNumbers
函数。boundGetNumbers()
调用时的this
是number
对象,并能够返回正确的数组对象。
可以将函数numbers.getNumbers
提取到变量simpleGetNumbers
中而不进行绑定。在之后的函数调用中simpleGetNumbers()
的this
是window
(严格模式下为undefined
),不是number
对象。在这个情况下,simpleGetNumbers()
不会正确返回数组。
6.2 紧密的上下文绑定
.bind()
创建一个永久的上下文链接,并始终保持它。一个绑定函数不能通过.call()
或者.apply()
来改变它的上下文,甚至是再次绑定也不会有什么作用。
只有绑定函数的构造函数调用才能更改已经绑定的上下文,但是很不推荐的做法(构造函数调用必须使用常规的非绑定函数)。
下面示例创建一个绑定函数,然后尝试更改其已预先定义好的上下文
function getThis() { 'use strict'; return this;}const one = getThis.bind(1);// 绑定函数调用one(); // => 1// 使用带有.apply()和.call()的绑定函数one.call(2); // => 1one.apply(2); // => 1// 再次绑定one.bind(2)(); // => 1// 以构造函数的形式调用绑定函数new one(); // => Object
'use strict';
return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// 使用带有.apply()和.call()的绑定函数
one.call(2); // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的形式调用绑定函数
new one(); // => Object
只有new one()
改变了绑定函数的上下文,其他方式的调用中this
总是等于1。
7. 箭头函数
箭头函数用于以更短的形式声明函数,并在词法上绑定上下文。它可以这样使用
const hello = (name) => { return 'Hello ' + name;};hello('World'); // => 'Hello World'// Keep only even numbers[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6](name) => {
return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// Keep only even numbers
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
箭头函数语法简单,没有冗长的function
关键字。当箭头函数只有一条语句时,甚至可以省略return
关键字。
箭头函数是匿名的,这意味着name
属性是一个空字符串''
。这样它就没有词法上函数名(函数名对于递归、分离事件处理程序非常有用)
同时,跟常规函数相反,它也不提供arguments
对象。但是,这在ES6中通过rest parameters
修复了:
const sumArguments = (...args) => { console.log(typeof arguments); // => 'undefined' return args.reduce((result, item) => result + item);};sumArguments.name // => ''sumArguments(5, 5, 6); // => 16(...args) => {
console.log(typeof arguments); // => 'undefined'
return args.reduce((result, item) => result + item);
};
sumArguments.name // => ''
sumArguments(5, 5, 6); // => 16
7.1. 箭头函数中的this
this 定义箭头函数的封闭上下文
箭头函数不会创建自己的执行上下文,而是从定义它的外部函数中获取 this
。换句话说,箭头函数在词汇上绑定 this
。
下面的例子说明了这个上下文透明的特性:
class Point { constructor(x, y) { this.x = x; this.y = y; } log() { console.log(this === myPoint); // => true setTimeout(()=> { console.log(this === myPoint); // => true console.log(this.x + ':' + this.y); // => '95:165' }, 1000); }}const myPoint = new Point(95, 165);myPoint.log();
constructor(x, y) {
this.x = x;
this.y = y;
}
log() {
console.log(this === myPoint); // => true
setTimeout(()=> {
console.log(this === myPoint); // => true
console.log(this.x + ':' + this.y); // => '95:165'
}, 1000);
}
}
const myPoint = new Point(95, 165);
myPoint.log();
setTimeout
使用与log()
方法相同的上下文(myPoint
对象)调用箭头函数。正如所见,箭头函数从定义它的函数继承上下文。
如果在这个例子里尝试用常规函数,它创建自己的上下文(window
或严格模式下的undefined
)。因此,要使相同的代码正确地使用函数表达式,需要手动绑定上下文:setTimeout(function(){…}.bind(this))
。这很冗长,使用箭头函数是一种更简洁、更短的解决方案。
如果箭头函数在最顶层的作用域中定义(在任何函数之外),则上下文始终是全局对象(浏览器中的 window):
onst getContext = () => { console.log(this === window); // => true return this;};console.log(getContext() === window); // => true
console.log(this === window); // => true
return this;
};
console.log(getContext() === window); // => true
箭头函数一劳永逸地与词汇上下文绑定。即使修改上下文,this
也不能被改变:
const numbers = [1, 2];(function() { const get = () => { console.log(this === numbers); // => true return this; }; console.log(this === numbers); // => true get(); // => [1, 2] // Use arrow function with .apply() and .call() get.call([0]); // => [1, 2] get.apply([0]); // => [1, 2] // Bind get.bind([0])(); // => [1, 2]}).call(numbers);1, 2];
(function() {
const get = () => {
console.log(this === numbers); // => true
return this;
};
console.log(this === numbers); // => true
get(); // => [1, 2]
// Use arrow function with .apply() and .call()
get.call([0]); // => [1, 2]
get.apply([0]); // => [1, 2]
// Bind
get.bind([0])(); // => [1, 2]
}).call(numbers);
无论如何调用箭头函数get
,它总是保留词汇上下文numbers
。用其他上下文的隐式调用(通过 get.call([0])
或get.apply([0])
)或者重新绑定(通过.bind()
)都不会起作用。
箭头函数不能用作构造函数。将它作为构造函数调用(new get()
)会抛出一个错误:TypeError: get is not a constructor
。
7.2. 陷阱: 用箭头函数定义方法
你可能希望使用箭头函数来声明一个对象上的方法。箭头函数的定义相比于函数表达式短得多:(param) => {...} instead of function(param) {..}
。
来看看例子,用箭头函数在Period类上定义了format()
方法:
function Period (hours, minutes) { this.hours = hours; this.minutes = minutes;}Period.prototype.format = () => { console.log(this === window); // => true return this.hours + ' hours and ' + this.minutes + ' minutes';};const walkPeriod = new Period(2, 30); walkPeriod.format(); // => 'undefined hours and undefined minutes'
this.hours = hours;
this.minutes = minutes;
}
Period.prototype.format = () => {
console.log(this === window); // => true
return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'
由于format
是一个箭头函数,并且在全局上下文(最顶层的作用域)中定义,因此 this
指向window
对象。
即使format
作为方法在一个对象上被调用如walkPeriod.format()
,window
仍然是这次调用的上下文。之所以会这样是因为箭头函数有静态的上下文,并不会随着调用方式的改变而改变。
该方法返回'undefined hours和undefined minutes'
,这不是咱们想要的结果。
函数表达式解决了这个问题,因为常规函数确实能根据实际调用改变它的上下文:
function Period (hours, minutes) { this.hours = hours; this.minutes = minutes;}Period.prototype.format = function() { console.log(this === walkPeriod); // => true return this.hours + ' hours and ' + this.minutes + ' minutes';};const walkPeriod = new Period(2, 30); walkPeriod.format(); // => '2 hours and 30 minutes'
this.hours = hours;
this.minutes = minutes;
}
Period.prototype.format = function() {
console.log(this === walkPeriod); // => true
return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => '2 hours and 30 minutes'
walkPeriod.format()
是一个对象上的方法调用,它的上下文是walkPeriod
对象。this.hours
等于2
,this.minutes
等于30
,所以这个方法返回了正确的结果:'2 hours and 30 minutes'
。
总结
为函数调用对this
影响最大,从现在开始不要问自己:
this 是从哪里来的?
而是要看看
函数是怎么被调用的?
对于箭头函数,需要想想
在这个箭头函数被定义的地方,this是什么?
这是处理this
时的正确想法,它们可以让你免于头痛。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
交流
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。