js基础梳理-关于this常见指向问题的分析
首先,依然回顾《js基础梳理-究竟什么是执行上下文栈(执行栈),执行上下文(可执行代码)?》中的
3.执行上下文的生命周期
3.1 创建阶段
- 生成变量对象(Variable object, VO)
- 建立作用域链(Scope chain)
- 确定this指向
3.2 执行阶段
- 变量赋值
- 函数引用
- 执行其他代码
我们已经梳理了在执行上下文中的变量对象是如何生成的以及作用域链是如何建立的。本篇文章就继续梳理下,执行上下文的this指向是如何确定的。
首先,执行上下文分全局执行上下文和函数执行上下文,在浏览器环境的全局执行上下文中,this指向全局对象,也就是window(接下来本篇文章都仅只浏览器环境)。这也相对简单,没有那么多复杂的情况需要考虑。
而在函数执行上下文中,确定this指向发生在函数执行上下文的创建阶段,而函数执行上下文又是在函数被调用后才产生的。因此,不难理解:this的指向,是在函数被调用的时候确定的。而不是函数声明的时候确定的。而确定this的指向难就难在函数被调用的方式是多种多样的,所以我们就需要从函数执行的各种方式分别去分析this的指向。
1. this与普通函数执行(默认绑定)
- 当一个函数执行不带任何修饰时,使用默认绑定规则。
- 默认绑定:函数体如果在非严格模式下,this绑定到window,严格模式下绑定到undefined。
// 1.1 函数体在非严格模式下的全局函数执行
function fn () {
console.log(this)
}
fn1() // => window
// 1.2 函数体在严格模式下的全局函数执行
'use strict'
function fn () {
console.log(this)
}
fn() // => undefined
// 1.3 函数体在非严格模式下的函数中的函数执行
function fn1 () {
function fn2 () {
console.log(this)
}
fn2()
}
fn1() // => window
// 1.4 函数体在严格模式下的函数中的函数执行
'use strict'
function fn1 () {
function fn2 () {
console.log(this)
}
fn2()
}
fn1() // => undefined
// 1.5 函数体在非严格模式下,而函数调用在严格模式下时, this依然指向window
function fn () {
console.log(this)
}
(function () {
'use strict'
fn() // => window
})()
2. this与对象中的方法执行(隐式绑定)
2.1 无论是否是严格模式,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
// 2.1.1函数直接在对象中声明
var obj = {
a: 1,
test: function () {
console.log(this.a)
}
}
obj.test(); // => 1
// 2.1.2 函数先声明,再由对象引用
function test () {
console.log(this.a)
}
var obj = {
a: 1,
test: test
}
obj.test(); // => 1
2.2 对象属性引用链中只有最顶层或者说只有最后一层会影响调用位置
// 2.2.1 多层对象引用,this指向离函数调用最近的对象
function test () {
console.log(this.a)
}
var obj2 = {
a: 2,
test: test
}
var obj1 = {
a: 1,
obj2: obj2
}
obj1.obj2.test() // => 2
2.3 隐式丢失:被隐式绑定的函数可能会丢失绑定对象。
// 2.3.1 将obj.foo当作函数别名赋值给一个变量
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函数别名
var a = '全局属性'
bar() // => 全局属性
在2.3.1中,虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar() 其实是一个不带任何修饰的普通函数调用。因此也使用默认绑定规则。
// 2.3.2 将obj.foo当作bar的回调函数。
function foo () {
console.log(this.a)
}
function bar (fn) {
fn()
}
var obj = {
a: 2,
foo: foo
}
var a = '全局属性'
bar(obj.foo) // => 全局属性
大家都知道所有的函数的参数都是按值传递的,(都是栈内数据的拷贝)。
基本类型传的是值本身(因为直接把值存在栈内),引用类型传的是对象在内存里面的地址(因为复杂对象在堆内,所以在栈里存对象所在的堆地址)。
因此 bar(obj.foo) 执行时,参数fn实际上引用的是foo。而foo函数执行其实就是一个不带任何修饰的普通函数调用。所以它也使用默认绑定规则。
2.3.4
由此可扩展到 setInterval, setTimeout,以及匿名函数中的this也是使用的默认绑定规则。即非严格模式下,this指向window,严格模式下,this指向undefined。
3.this与call,apply,bind(显示绑定)
显示绑定规则:this指向第一个参数。
3.1 call
// 3.1.1
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 12
}
xw.say.call(xh, "实验小学", "六年级") // => 小红 , 女 ,今年12 ,在实验小学上六年级
在3.1.1代码示例中,当调用say时强制把它的this绑定到了xh上。
3.2 apply
// 3.2.1
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
console.log(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 12
}
xw.say.apply(xh,["实验小学","六年级"]) // => 小红 , 女 ,今年12 ,在实验小学上六年级
3.3 bind
// 3.3.1
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 12
}
xw.say.bind(xh)("实验小学","六年级") // => 小红 , 女 ,今年12 ,在实验小学上六年级
通过以上这些例子,其实也可以明显的看到call,apply,bind的区别。
3.4 显示绑定的变种-硬绑定
之前说到隐式丢失的问题,而显示绑定的一个变种可以解决隐式丢失的问题,这种方式被称之为硬绑定。
// 2.3.2 将obj.foo当作bar的回调函数。
function foo () {
console.log(this.a)
}
function bar (fn) {
fn()
}
var obj = {
a: 2,
foo: foo
}
var a = '全局属性'
bar(obj.foo) // => 全局属性
将其修改成
// 3.4.1 利用call方法解决隐式丢失的问题
function foo(){
console.log(this.a);
}
function bar(fn){
fn.call(obj);
}
var obj = {
a:2,
foo:foo
}
var a = "全局属性";
bar(obj.foo); // => 2
这里依旧是创建了 bar()这个函数,但是在其内部手动调用了obj.foo.call(obj),把foo强制绑定到了obj对象,之后无论如何调用bar(),它总会手动在obj上调用foo.
再看看隐式丢失的代码示例2.3.1
// 2.3.1 将obj.foo当作函数别名赋值给一个变量
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo // 函数别名
var a = '全局属性'
bar() // => 全局属性
将其修改成
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo.bind(obj);
var a = "global";
bar(); // => 2
总结下“显示绑定三人组”
共同点:
1、都用于控制this指向;
2、第一个参数都是this需要指向的对象,也就是上下文;
3、都可以后续参数传递;
4、没有任何参数时或者第一个参数是null时,this都指向全局对象window >
区别:
1、call后面的参数与say方法中的参数是一一对应的,而apply的第二个参数是一个数组,数组中的元素是和say方法中一一对应的。所以当传入的参数数目不确定时,多使用apply。
2、call、apply绑定后立刻执行,bind是延迟执行。换言之,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,就使用bind()方法吧
扩散性思考:
call,apply,bind还有什么实际使用场景?它们的原理是什么?如何自己模拟实现这三个方法?
4. this与构造函数调用(new 绑定)
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[原型]]链接。
- 这个新对象会绑定到函数调用的this.
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
在这里主要关心第1,3,4步。
function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a) // 2
使用new来调用Foo(...)时,会构造一个新对象并把它绑定到foo(...)调用中的this上,new是最后一种可以影响到函数调用时this绑定行为的方法,称之为new绑定。
5.this与箭头函数
5.1箭头函数中的this指向包裹它的第一个普通函数中的this
// 5.1 箭头函数外部是普通函数调用时
function fn () {
return () => {
return () => {
console.log(this)
}
}
}
fn()()() // => window
在这个例子中,因为包裹箭头函数的第一个普通函数是 fn,而fn的this指向window, 所以箭头函数中的 this也是 window。
以上的例子,多数为单个规则。
这里推荐一种方法:
- 先看看它定义时,是不是在箭头函数内。如果是,那么this就指向包裹它的第一个普通函数中的this。
- 不管它是不是箭头函数,我们都需要继续寻找它的this指向,再看它的调用方式。如果它是被 new 调用,那么this指向实例。
- 如果调用的时候看到了bind,call,apply。那么大部分时候this都指向第一个参数。
- 如果调用时既没有看到new,也没有看到bind,call,apply,那么大部分时候this指向window(非严格模式下)。
在写这篇文章的时候,本来标题是关于this指向的全面分析,后来想想,其实也并不全面。真正的宗师级别,会是多个规则夹在一起去分析this,那样的话,篇幅肯定太长了,而且一时半会其实也记不了那么多。而且就这篇文章所讲的,其实也够大家去理解分析大部分基础的this指向问题了。有兴趣的话,可以自己去翻阅一下《你不知道javascritp·上卷》这本书,里面还有一些我个人觉得比较少见而没有列出来的this指向问题,比如我的文章中提到了硬绑定,那其实也会有软绑定。
在这里也贴一篇蚂蚁金服前端的博文,是一个多个规则综合应用判断this指向的题。很多时候,当我们以为自己懂了,而总是会有人更深入的去挖掘我们之前没有想到的知识点,学无止境。