简介
在上一篇文章中我们记录了执行栈
、执行上下文
、执行上下文生命周期
、this的产生
等等,在这一篇文章中我们来记录一下this的绑定
也就是this
的值确定。this
在创建阶段
被创建(确定默认值),但是在执行阶段
会改变this
的值。所以一般我们都会说确定this
是在执行阶段
。
本篇文章章节大致如下:
- 多种绑定
this
方式 - 改变
this
方式new
、Object.create
- 另外三种改变
this
的方式bind
、call
、apply
- 异类
箭头函数
- 优先级
下面我们就慢慢开始一步一步了解this
。
多种绑定this
方式
无论是默认的绑定this
的规则,还是后面改变this
的方法,我们尽量深入的记录,大致目录如下:
- 默认绑定
- 显示绑定
- 隐式绑定
- bind、call、apply 绑定
- new 绑定、Object.create()绑定
- 箭头函数
我们都知道this
的值是在运行时绑定的,并且谁调用它,它指向谁。下面我们就开始一步一步的了解this
绑定的细节和实现。
默认绑定
默认绑定其实就是在全局中声明函数,并且在全局中调用函数,这样它不会受到任何调用对象和修饰符的干扰。代码如下:
1 | function defalutsFunc() { |
在普通模式下,如果我们是在浏览器端运行代码,this
指向window
。它在严格模式下this
会是undefined
。
默认绑定多种方式:
- 全局调用函数
- IIFE(自执行函数)
- 匿名函数
隐式绑定
隐式绑定,其实就是当前调用函数的this
会指向当前调用该函数的执行上下文。隐式绑定this
是不可靠的,他会因为调用者的不同而不同。
1 | function globalFunc() { |
在scopeObj.scopeFunc()
我们其实是在scopeObj
这个作用域
中调用scopeFunc
,这样scopeFunc
的this
指向scopeObj
创建的上下文。 但是隐式绑定在传递过程中会丢失this
,其实还是看调用它的执行上下文是那个,它的this
就会指向当前执行上下文。修改代码如下:
1 | var funcObj = scopeObj.scopeFunc; |
我们又把scopeObj.scopeFunc
赋值给了一个普通变量funcObj
,在全局作用域中调用了赋值的这个funcObj
,所以funcObj
的 this 指向了全局
的执行上下文。
其实
参数
也是一样的效果。
显示绑定
因为隐式绑定的丢失问题,所以有了后面的显示绑定call
、apply
、bind
等等的方式。
我们可以通过call
、apply
它们可以显示的改变this
的绑定.
call
:fun.call(thisArg, arg1, arg2, ...)
第一个参数this
要绑定的值,后面多个参数是要传入方法的参数。apply
:func.apply(thisArg, [argsArray])
第一个参数this
要绑定的值,可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。
1 | function globalFunc(age) { |
但是即使通过call
orapply
改变的this
值也是会丢失的,在传递的过程中,其实通过显示绑定也并不能保证我们的this
一直是绑定的一个值。
我们可以通过在外层包裹一层函数来绑定this
,示例代码如下:
1 | function scopeFunc() { |
可以看到我们通过simpleBind
中返回一个匿名函数,这样通过call
或apply
它也只能改变外部匿名函数的this
,在匿名函数内部我们通过fn.call(obj)
给方法默认绑定一个this
,这个只是一个简单的bind
实现。
也可以直接通过Function.prototype.bind
来实现,bind
返回一个硬绑定的函数。
1 | function scopeFunc(args) { |
还有一种方式就是高阶函数,我们传入一下函数来获取当前执行上下文,比如map
、forEach
等等。
1 | var aData = [ |
它的内部也是使用了call
或者apply
来改变传入函数的this
。如果有兴趣去看一下另一篇博客Array 常用的方法和实现 reduce、map、filter、forEach来深入了解一下。
new 绑定、Object.create()绑定
new运算符
和Object.create()
方法也是可以改变this
的指向的。
首先我们要理解new运算符
它具体做了什么操作,大致过程如下:
- 创建一个空的简单 JavaScript 对象(即
{}
); - 链接该对象(即设置该对象的构造函数)到另一个对象 ;
- 将步骤 1 新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
可以看到在new 运算符
中有修改过this
的指向,下面我们通过一个示例代码来了解一下。
1 | function globalFunc() { |
我们通过new 运算符
调用globalFunc
时,我们可以看到globalFunc
中的name
在exampleFunc
中也可以访问到相同的name
。
其实我们也可以通过Object.create()
来也可以实现一样的效果,代码如下:
1 | function globalFunc() { |
其实Object.create
内部和 new 有点类似,但是Object.create
它调用的是new Func()
用于生成一个对象实例。
箭头函数
箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)
this 对象的指向是可变的,但是在箭头函数中,它是固定的。
普通函数与箭头函数的对比如下表所示:
对比 | 普通函数 | 箭头函数 |
---|---|---|
this 指向规则 |
this 总是指向调用它的那个对象 |
1.所有箭头函数本身没有this 2.箭头函数的 this 在定义的时候捕获自外层第一个普通函数的 this 3.如果箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向window (全局对象) |
有无prototype |
有 | 箭头函数没有prototype(原型) |
可否new |
可以 | 箭头函数作为匿名函数,是不能作为构造函数的(因为箭头函数没有constructor ),不能使用 new,不然会报错 |
有无arguments |
有 | 1.箭头函数的this 指向全局,使用会报未声明的错误 2.箭头函数的 this 指向普通函数时,它的argumens继承于改普通函数 |
可否new |
可以 | 箭头函数作为匿名函数,是不能作为构造函数的(因为箭头函数没有constructor ),不能使用 new,不然会报错 |
可否改变this 指向 |
可以通过call、apply、bind 改变this 的指向 |
箭头函数本身的this 指向不能改变,但是可以修改它要捕获的对象的this |
如果有兴趣的话可以去看另一篇arrow-functions(箭头函数)和普通的函数的区别 this(二)
注意事项
new 注意事项
new
可以很方便构造调用一个函数并且声称一个实例,但是如果我们使用不太小心的话也会带来很多不必要的麻烦。
忘记写 new运算符,那样我们就得不到我们想要的结果,如果实在全局环境中,那么函数的this
会绑定到全局。如果实在严格模式this
会绑定为undefined
。
bind/call、bind 注意事项
把null
或者undefined
作为this
的绑定对象传入call、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认规则。
软绑定
硬绑定可以把this
强制绑定到指定的对象(new除外
),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this
。
如果给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改 this 的能力。
1 | if (!Function.prototype.softBind) { |
使用:软绑定版本的foo()
可以手动将this
绑定到obj2
或者obj3
上,但如果应用默认绑定,则会将this
绑定到obj
。
1 | function foo() { |
绑定优先级
我们对比显示绑定
、隐式绑定
、new 绑定
、默认绑定
,不包含箭头函数
的对比,因为箭头函数
它本身没有this
,它会从它外层的普通函数或者全局获取。
大致对比过程:
显示绑定
与隐式绑定
对比默认绑定
与隐式绑定
对比new 绑定
与显示绑定
对比
我们就通过代码一步一步的记录。
显示绑定
与隐式绑定
我们直接通过代码来对比。
1 | function test() { |
通过上面的代码我们知道了显示绑定 > 隐式绑定
。
默认绑定
与隐式绑定
默认绑定
与隐式绑定
对比直接上代码
1 | var name = 'GlobalName'; |
我们首相声明一个函数test
,然后把这个函数赋值给一个对象yinObj
的func
属性,然后我们分别在全局和对象中调用test
函数,在全局中调用test
函数得出的结果是GlobalName
,yinObj.func()
得出的结果是yinObj
。可以得出结果隐式绑定>默认绑定
。
new 绑定
与显示绑定
我们通过代码看new 绑定
与显示绑定
他们之间的优先级。
1 | function foo(name) { |
在 JavaScript
内部,会判断硬绑定函数是否是被 new
调用,如果是的话就会使用新创建的 this
替换硬绑定的 this
。
总结
this
在创建阶段
被创建(确定默认值),但是在执行阶段
会改变this
的值。所以一般我们都会说确定this
是在执行阶段
。
在本篇文章中我们知道了多种绑定方式如下:
- 默认绑定:多种绑定方式全局调用函数、IIFE(自执行函数)、匿名函数
- 显示绑定:可以通过
call
、apply
、bind
来显示改变this
绑定。 - 隐式绑定:通过赋值的方式实现隐式绑定。但是很容易丢失。
- bind、call、apply 绑定:和显示绑定相同。
- new 绑定、Object.create()绑定:和显示绑定相同。
- 箭头函数:
ES6
的实现,它本身没有this
,它的this
从外层普通函数或者全局获取。
多种绑定方式的优先级:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
下一篇文章我们自己来实现多种绑定放法。
参考
如何理解 javascript 中 this 的绑定?
JavaScript 深入之史上最全–5 种 this 绑定全面解析