无论是在java中还是在JavaScript中,this的指向一直是值得仔细思考的问题。最近做题也遇到不少相关的问题,所以在这里小小的总结下下,如有不正确,欢迎大家指正,感谢!!!希望我们一起学习进步,O(∩_∩)O哈哈~。
首先,与变量求值时候的词法作用域不同, this求值时候,是采用的动态作用域,总结起来,即this指向其调用者。
具体地,可将this的调用可以分为下面五种情况:
- 普通函数
- 方法函数
- 构造函数/class函数及其super
- 通过bind、call、apply绑定this的函数
- 箭头函数
1. 当在普通函数调用的时候,this指向widow
function fn () {
function in(){
console.log(this);
}
in(); // 内部this指向window
}
fn();
分析:
其执行环境不在window
下
其调用者也不是window
(非全局环境下作为默认函数调用是没有调用者的)。然而this
却指向window
, 这是不合理的。JavaScript
是一门在不断进化的语言,一开始的设计存在着诸多开放性问题,除了函数调用时this
的指向问题,还有诸如重复声明,以及未声明便可以初始化的问题。
为了消除JS
语法的一些不严谨、不安全之处,ECMAscript 5添加了第二种运行模式: 严格模式。
在严格模式下,此处this
会合理的指向,console.log
打印出来的将是undefined
(因为没有调用者)。
'use strict' // ---- 严格模式下!!!
function test() {
// 指向undefined
console.log(this)
}
test()
2. 当方法调用的时候this指向调用对象
注意: 这里的调用者是指执行时的直接调用者, 而不是声明时的储存位置 ! ! ! 如下:
var obj1 = {
test: function () {
console.log(this)
}
}
obj1.test() // 这里 this 指向obj
const obj2 = {}
// 将obj1的test方法引用给obj2的 fn
obj2.fn = obj1.test
// 调用者为obj2, 故这里 this指向obj2
obj2.fn()
还有一种特殊情况,
(0, obj1.test)() // this指向全局window
为什么会发生这种 this 丢失
的现象呢, 其实(0, obj1.test)
这个表达式的返回值即obj1.test
指向的 -- > 源函数, 实际上进行的是普通函数调用, 而不是方法调用。其相当于:
fn = obj1.test>
fn()
3 . 构造函数调用指向实例对象
JavaScript中, 当使用new
关键字对一个函数进行实例化时, 其函数会作为构造函数调用, 此时, 构造函数内部的this指向一个隐式创建的空对象, 最终若没有显式的返回对象的话, 则会默认返回这个隐式创建的对象。
new 的过程大致可以分为下面四个步骤:
- 隐式创建一个内部的空对象
- 将内部对象的原型与构造函数的原型链连接
- 执行构造函数内部代码, 此时this指向内部对象
- 若没有显式的返回对象时, 返回我们的内部对象。(若有显式的返回对象时, 则按普通函数返回)
其中,class属于es6的新特性, 与ES5中的构造函数没有本质上的区别, 其constructor
及其中super的this指向依旧是我们的内部对象。
4. 通过bind、call、apply绑定this的函数
可以通过bind
、call
、apply
改变 this 的指向。
这是挂载在函数对象原型链上的三种方法(Function.prototype.bind
, Function.prototype.call
, Function.prototype.apply
)。
其中call
, apply
改变函数调用时的this指向,即将this 指向 其第一个参数,这两个方法的不同在于第二个参数的形式不同。apply()会以数组的形式作为【原函数】的参数调用, call() 会以普通参数的形式作为【原函数】的参数调用,举个栗子:
obj1.fn.call(obj2,[1,2,3,4])
bj1.fn.apply(obj2,1,2,3,4)
这两种情况下, 1,2,3,4 作为函数fn的参数,this指向obj2。
call 的性能较 apply 要更好, 速度更快。参考:https://zhuanlan.zhihu.com/p/27659836
bind 只接受一个参数作为this的指向, 然后返回一个绑定了this
的函数, 返回的函数除了this绑定了以外, 内部逻辑与原函数完全相同; bind方法没有副作用, 不会修改原函数。
当然, 部分浏览器上(举个栗子:Chrome) bind的性能开销较大,速度较慢, 有的框架会选择实现自己的bind, 如 Vue, 具体代码如下:
/**
* Simple bind, faster than native
* 为什么原生的bind慢,可以看(https://stackoverflow.com/questions/8656106/why-is-function-prototype-bind-slow)
* 总结一下就是, 原生bind会判断一下`this instance of bound`如果是就实例化`bound`(不是目标函数, 而是bind自身)函数
* **instance**的判断造成了性能损失(instance 是沿着原型链遍历查找的)
*/
export function bind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l: number = arguments.length
/**
* 在部分运行环境中call速度要比apply更快
* 所以这里做了一个判断, 而不是全部直接都用apply
*/
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
// record original fn length
/**
* 记录原始函数的形参数量(原生的bind会绑定原始函数的形参数量)
* eg:
* function originFn (a, b, c) {} // 三个形参
* originFn.length // 3, 返回的是形参数量
* bind(originFn).length // 3, 返回的是上面原始函数的形参数量
* 而这里boundFn.length会返回1, 即其自身的形参数量,
* 故用_length来保留原始形参的数量, 以备以后可能用到
*/
boundFn._length = fn.length
return boundFn
}
5. 箭头函数
ES6中,通过箭头函数我们可以简单将函数的this指向绑定为外部函数的this, 举个栗子
var obj = {
age: 18,
changeAge: function (v) {
const arrowFn = () => {
this.age = 20
}
// 这里是作为普通函数调用, 其this理应指向window(非严格模式)或undefined(严格模式)
// 但是因为其声明时候属于箭头函数, 其this指向外部函数的this
// 即指向changeAge的this, 根据changeAge的调用方式而定
arrowFn()
}
}
// 此处changeAge的调用方式是方法调用, this指向调用者obj
// 故其内部的arrowFn函数的this也指向obj
obj.changeAge()
// 20
console.log(obj.age)
这里 this 的求值从动态作用域转换成了词法作用域, 不再是根据其直接的调用者而定, 而是指向其外部函数的调用者
var obj = {
age: 18,
changeAge: function (v) {
var that = this
function arrowFn () {
// 修改that, 即外部this的age属性
that.age = 20
}
arrowFn()
}
}
// 此处是方法调用, this指向调用者obj
obj.changeCount()
console.log(obj.age) // 20
所以对箭头函数使用apply
, call
, bind
方法, 或改变其调形式为方法调用或函数调用, 都无法改变箭头函数其 内部 this 的指向。
var originObj = {
number: 1,
getArrowFn: function () {
// 返回一个箭头函数, 此箭头函数调用时 返回this.number
return () => this.number
}
}
var obj = {
number: 2
}
// 在这一步, 箭头函数arrowFn内部this的指向已经确定了
// 指向其外部函数getArrowFn的调用者, 即originObj
const arrowFn = originObj.getArrowFn()
// 通过call、apply、bind无法改变箭头函数的this
arrowFn.call(obj) // 1
arrowFn.apply(obj) // 1
arrowFn.bind(obj)() // 1
// 通过函数调用或方法调用也无法改变箭头函数的this
arrowFn() // 1
obj.fn = arrowFn // 1
obj.fn()
最后 ,, 若通过 new 来将 箭头函数 当做一个构造函数进行实例化 会报错!!! 规范规定箭头函数是无法实例化的。