前言
在上篇文章《JS夯实之执行上下文与词法环境》中提到了词法环境的创建过程,跳过了ThisBinding
的绑定过程的陈述。而this的指向问题不管在面试或者业务工作中都是经久不衰的“坑”。
其实只要熟记四条准则,不论多么复杂的场景,你都可以正确判断出this
的指向。
隐式绑定
关键词:.
隐式绑定发生在对象方法调用的时候,即通过点标识符调用对象方法。此时方法内的this就指向点.
左边的对象:
var kid = {
name: '小明',
getName: function(){
return this.name
}
}
kid.getName() // 小明
这里需要注意一下函数别名的情况,如下:
var name = '小红'
var kid = {
name: '小明',
getName: function(){
return this.name
}
}
var fx = kid.getName
fx() // 小红
此时fx
中this
应该要用下文中“默认绑定”的场景来判定。
显式绑定
关键词:call
apply
bind
call
, apply
, bind
都是Function
原型链上的方法 —— Function.prototype.call
Function.prototype.apply
Function.prototype.bind
。它们都可以改变调用函数时this的指向。
在MDN中关于call
与apply
的解释:
调用一个给定this值的函数,并接收参数 call签名: (thisArg, arg1, arg2, ...) => any apply签名: (thisArg, [argsArray]) => any
call与apply之间的区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。在非严格模式下,thisArg
指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
使用call和apply调用函数,可以改变函数调用时this的指向:
function print({...args}){
console.log(this.name + ':' + args.join(',') )
}
var kid = {
name: '小明'
}
getName.call(kid, 1,2,3) // 小明:1,2,3
getName.apply(kid, [1,2,3]) // 小明:1,2,3
bind相比于call和apply,对待this则更为“粗暴”,bind方法会创建一个新的函数,新函数包装了原函数,并且这个新函数强绑定了this,调用这个新函数时将无视隐式绑定和默认绑定:
function print(){
console.log(this.name)
}
let kid = {
name: '小明',
}
let printName = print.bind(kid)
printName() // --> 小明
let kid2 = {
name: '小红',
getName: printName
}
kid2.getName() // --> 小明
如果bind后的函数被当做构造函数调用,则不受限制,this仍指向构建的新对象。
let kid = {
name: '小明'
}
function People(name){
this.name = name
}
const BindedPeople = People.bind(kid)
let kid2 = new BindedPeople('小红')
kid2.name // 小红
熟悉React的开发者会经常和bind
或箭头函数打交道,React在给子组件传递方法时,通常需要将方法的this绑定为父组件的实例,这样可以正确读取到父组件中一些属性和方法:
class Parent extends React.Component {
constructor() {
super();
this.name = 'i am parent'
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.name) // i am parent
}
render() {
return (
<Child onClick={this.handleClick}>click here</Child>
);
}
}
构造函数绑定
关键词:new
当一个函数被用作构造函数使用时(new Foo(...)
),则函数内的this指向实例化后的对象:
function Child(name){
this.name = name
this.getName = function(){
return this.name
}
}
let kid = new Child('小明')
kid.getName() // 小明
getName
中的this此时指向是对象kid
,可以读取到对象kid
上的name
属性。当然也可以使用隐形绑定的准则来判断出this
的指向。
关于
new
具体做了什么,可以移步《这是一篇关于JavaScript原型知识的还债帖》查看更多。 使用new
操作符实例化对象时,默认调用的函数的[[Construct]]
内部属性,ECMA中关于[[Construct]]
的描述。
默认绑定
将“默认绑定”放在其他三条规则之后说,是因为默认绑定是经过以上特定场景准则后仍无法判断this绑定时的兜底准则。一般发生在独立的函数调用和全局环境中。
var printThis = function(){
console.log(this)
}
printThis() // 输出结果视严格模式与否而定
在严格模式下,即在所有语句之前放一个特定语句 "use strict"时,以上代码输出为undefined
;
在非严格模式下,此时this指向全局对象 —— window | global
,代码输出为全局对象;
小结
四条准则按照优先级排列,应该是:构造函数绑定
--> 显式绑定
--> 隐式绑定
--> 默认绑定

箭头函数
在ES6中新增了一种函数定义方式——箭头函数,()=>{}
。箭头函数与普通函数相比,在一些内部属性访问上有很大的不同。引自ECMA中关于箭头函数的说明:
An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment.Typically this will be the Function Environment of an immediately enclosing function.
即箭头函数没有自己的arguments
super
this
new.target
内部属性(简称“四大护法”,😝),依据词法环境的outer引用的层层递进,在箭头函数中访问的arguments
super
this
new.target
都是读取自外层最近的普通函数的词法环境。箭头函数的this
始终指向函数定义时的this
,而非执行时。
由于没有“四大护法”的加持,箭头函数自然无法成为构造函数,也就无法使用new
来调用了。
proposal-bind-operator
tc39有一个提案proposal-bind-operator
,使用一个新的操作符::
来完成this的绑定操作。
obj::func
// 等价于:
func.bind(obj)
::obj.func
// 等价于:
obj.func.bind(obj)
obj::func(val)
// 等价于:
func.call(obj, val)
::obj.func(val)
// 等价于:
obj.func.call(obj, val)
借助babel插件可以提前使用::
运算符
最后
码字不易,如果:
- 这篇文章对你有用,请不要吝啬你的小手为我点赞;
- 有不懂或者不正确的地方,请评论,我会积极回复或勘误;
- 期望与我一同持续学习前端技术知识,请关注我吧;
- 转载请注明出处;
您的支持与关注,是我持续创作的最大动力!
本文首发于我的Blog仓库