this =?
在 JS 中,当一个函数执行时,都会创建一个执行上下文用来确认当前函数的执行环境,执行上下文分为 全局执行上下文
和 函数执行上下文
。而 this
就是指向这个执行上下文的对象。所以,this 是在运行时决定的,可以简单的理解为 谁调用,this 指向谁。
分四种情况来看:
- 普通函数调用
- 对象方法调用
- 构造函数调用
- call、apply、bind
普通函数调用
当函数作为函数独立调用的时候,则是在全局环境中运行,this
则指向全局对象 window
一个简单的例子
function demo() {console.log(this); // window
}
demo();
demo
函数独立调用,所以 this
指向全局对象 window
接着
function outer() {function inner() {console.log(this); // window
}
inner();}
outer();
虽然在 outer
函数内部声明了一个 inner
函数,但实际上 inner
函数是独立调用的,所以依然是在全局环境,this
仍然是指向了 window
。
function demo(func) {func();
}
demo(function () {console.log(this); // window
});
给 demo
函数传入一个匿名函数,执行匿名函数 func
的时候,依然是作为函数独立调用,所以 this
仍然指向window
。
理解一下什么是作为函数独立调用:
当定义一个函数,例如 var demo = function () {}
等号右边的函数是独立放在 内存
中的,然后赋予 demo 变量的指向为函数所在的 内存地址
,当直接调用 demo()
,相当于直接找到函数本身执行,所以函数内部创建的上下文为全局上下文,this
则指向了全局对象 window
。
对象方法调用
当调用一个对象方法时,this
代表了对象本身。
let obj = {
name: 'invoker',
getName: function () {console.log(this); // obj
console.log(this.name); // "invoker"
}
}
obj.getName();
定义了一个 obj
对象,调用其内部的getName
,this
则指向了 obj
对象。
稍微修改一下
var name = 'windowName';
let obj = {
name: 'invoker',
getName: function () {console.log(this); // window
console.log(this.name); // windowName
}
}
var getName = obj.getName;
getName();
当用一个变量 getName
接收 obj
对象的 getName
方法,再执行 getName
,发现 this
指向了 window
,因为此时变量 getName
直接指向了函数本身,而不是通过 obj
去调用,此时就变成了函数独立调用的情况了。
再看个例子
let obj = {test: function() {function fn() {console.log(this); // window
}
fn();},
test1: function (fn) {fn()
}
}
obj.test();
obj.test1(function () {console.log(this) // window
});
虽然在 obj
对象的 test
方法内定义了 fn
,但执行时同样属于函数独立调用,所以 this
指向 window
。
将函数作为参数传入 obj
的 test1
方法,也属于函数独立调用,this
同样指向 window
。
构造函数调用
使用 new
关键字调用函数,则是构造函数调用,this
指向了该构造函数新创建的对象。
function person(name) {this.name = name}
let p = new person('invoker')
console.log(p.name) // 'invoker'
回顾一下 new
关键词的过程:
- 创建一个新的对象
obj
- 使得
obj
的__proto__
指向 构造函数的原型对象 - 执行构造函数中的
constructor
,改变this
的指向为obj
- 如果结果是对象类型,则返回结果,否则返回 obj
function myNew(Fn) {let obj = {}
obj.__proto__ = Fn.prototype
const res = Fn.prototype.constructor.call(obj)
if (typeof res === 'object') {obj = res}
return obj
}
call、apply、bind
this
指向的是 call
、apply
、bind
调用时传递的第一个参数。
let obj = {name: 'invoker'}
function demo() {console.log(this.name) // 'invoker'
}
demo.call(obj)
demo.apply(obj)
demo.bind(obj)()
箭头函数
箭头函数在执行时并不会创建自身的上下文,它的 this
取决于自身被定义的所在执行上下文。
例子:
let obj = {fn: () => {console.log(this) // window
}
}
obj.fn()
obj
的 fn
指向一个箭头函数,由于只有函数可以创建执行上下文,而箭头函数外部并没有包裹函数,所以箭头函数所在的执行上下文为全局的执行上下文,this
指向 window
包裹一个函数看看呗?
let obj = {fn: function () {console.log('箭头函数所在执行上下文', this) // '箭头函数所在执行上下文' obj
var arrow = () => {console.log(this) //obj
}
arrow()}
}
obj.fn()
箭头函数 arrow
被定义在 obj.fn
内,所以 fn
中的 this
就是 arrow
中的 this
箭头函数一次绑定上下文后便不可更改:
let obj = {name: 'invoker'}
var demo = () => {console.log(this) // window
}
demo.call(obj)
虽然使用了 call
函数间接修改 this
的指向,但并不起作用。
为什么会有 this 的设计
javascript 中存在 this
的设计,跟其内存中的数据结构有关系。
假设定义 let obj = {name: 'invoker'}
- 此时会先生成一个对象
{name: 'invoker'}
并放在内存当中 - 将
{name: 'invoker}
所在的内存地址赋予obj
所以 obj
其实就是个指向某个对象的地址,如果要读取 obj.name
,则先要找到 obj
所在地址,然后从地址中拿到原始对象,读取 name
属性。
对象中每个属性都有一个属性描述对象:可通过 Object.getOwnPropertyDescriptor(obj, key)
来读取。
也就是说上面所说的 obj
的 name
属性实际是下面这样的
{
name: {[[value]]: 'invoker',
[[configurable]]: true,
[[enumerable]]: true,
[[writable]]: true
}
}
value
就是获得的值。
现在假设对象的属性是一个函数:
let name = 'windowName'
let obj = {
name: 'invoker',
sayHello: function () {console.log('my name is' + this.name)
}
}
let descriptor = Object.getOwnPropertyDescriptor(obj, 'sayHello')
console.log(descriptor)
// 这个 sayHello 的属性描述对象为:
sayHello: {[[value]]: ƒ (),
[[configurable]]: true,
[[enumerable]]: true,
[[writable]]: true
}
sayHello
的 value
值是一个函数,这个时候,引擎会单独将这个函数放在 内存
当中,然后将函数的内存地址赋予 value
。
因此可以得知,这个函数在 内存
中是单独的,并不被谁拥有,所以它可以在不同的上下文执行。
由于函数可以在不同上下文执行,所以需要一种机制去获取当前函数内部的执行上下文。所以,就有了 this
,它指向了当前函数执行的上下文。
// 接着上面代码
obj.sayHello() // my name is invoker
let sayHello = obj.sayHello
sayHello() // my name is windowName
obj.sayHello()
是通过 obj
找到 sayHello
,也就是对象方法调用,所以就是在 obj
环境执行。
当 let sayHello = obj.sayHello
,变量 sayHello
就直接指向函数本身,所以 sayHello()
也就是函数独立调用,所以是全局环境执行。
总结
this
的出现,跟 JS 引擎内存中的数据结构有关系。
当发现一个函数被执行时,通过上面的多种情况。
- 分析函数怎么调用(单独调用、对象方法、构造方法)
- 是否有使用
call、apply
等间接调用 - 是否有箭头函数
- 甚至还可能分析是否为严格模式
这样就能很好的确认 this
的指向。