JavaScript中的this全面解析

this全面解析

每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)

调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中,调用位置决定了this的绑定。

function baz() { 
    // 当前调用栈是:baz 
    // 因此,当前调用位置是全局作用域 
    console.log( "baz" ); 
    bar(); // <-- bar 的调用位置 
}
function bar() {
    // 当前调用栈是 baz -> bar 
    // 因此,当前调用位置在 baz 中 
    console.log( "bar" ); 
    foo(); // <-- foo 的调用位置 
}
function foo() { 
    // 当前调用栈是 baz -> bar -> foo 
    // 因此,当前调用位置在 bar 中 
    console.log( "foo" ); 
}
baz(); // <-- baz 的调用位置

绑定规则

在找到调用位置后,应用四条规则中的某一条,完成this的绑定

1. 默认绑定

我们可以把这条规则看做无法使用其他规则时的默认规则;

直接使用不带任何修饰的函数引用, 在调用时, 只能使用 默认绑定;

默认绑定 会将 this 绑定到 window 对象上;(this === window)

function foo() {
  console.log(this === window)

  bar() // true
}

function bar() {
  console.log(this === window)
}

foo() // true

但在严格模式下, 默认绑定会绑定一个 undefined:

"use strict"

function foo() {
  console.log(this)
}

foo() // undefined
2. 隐式绑定

通过考虑 调用位置是否具有上下文对象, 或者说是否被某个对象拥有包含, 来判断是否运用这条规则:

如下示例, 我们声明一个 foo 函数, 并将其赋值给 obj 对象的 foo 属性:

function foo() {
    console.log(this.a)
}

var obj = {
    a: 1,
    foo: foo
}

obj.foo() // 1
		  // 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象

foo()     // undefined

对象属性引用链中只有最后一层在调用位置中起作用, 如下:

function fn() {
    console.log(this.msg)
}

var child = {
    msg: 'child',
    fn: fn
}

var parent = {
    msg: 'parent',
    child: child
}

parent.child.fn() // child
隐式丢失

如下示例, o.foothis 隐式绑定在了 o 对象上, 而 bar 引用了 o.foo 函数本身,相当于把foo()函数的地址值赋给了变量bar, 所以此时的 bar() 其实是一个不带任何修饰的函数调用, 因此使用了 默认绑定 规则:

var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}

var bar = o.foo

o.foo() // 1
bar()   // undefined

另一个很出乎意料的例子, 示例中, bar(o.foo) 实际上采用了隐式赋值: callback = o.foo, 事实上跟上面的例子一样, 都是直接引用了 o.foo 函数本身, 所以造成了 隐式丢失:

造成隐式丢失的根本原因是,虽然用的是obj.fn,但由于js值传递的特性,实际赋值给变量的是函数的地址值,也就是变量本身引用的是函数的定义,与obj无关。

function bar(callback) {
    callback()
}

var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}

bar(o.foo) // undefined
3. 显式绑定

在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

当我们并不想通过 隐式 或者 默认 的方式来间接绑定 this 的指向, 我们需要强制的为函数指定一个绑定对象,通过使用 call()apply() 方法来实现;

fn.call(obj Object [, ...arguments])

第一个参数接收一个对象, 作为 this 关键字绑定的对象, 第二个参数是该函数传递的参数;

function foo(a, b) {
    console.log((a + b) * this.c)
}

var obj = {
    c: 2
}

foo.call(obj, 1, 2) // 6

// foo.apply(obj, [1, 2])
// call() 与 apply() 的效果完全一致, 唯一不同的只是传递参数的格式不同。apply接收数组,call接收,拼接的参数

但是显式绑定仍然无法解决丢失绑定问题。

使用显示绑定时, 我们重复进行绑定, 仍然会让之前的绑定值丢失:

var obj = { name: 'muzi' }

function foo() {
    console.log(this.name)
}

foo.call(obj)  // muzi

setTimeout(() => (
    foo.call({ name: 'yaya' } // yaya
)), 100)

如果我们想避免这种情况, 就需要使用 硬绑定;

硬绑定

硬绑定的典型应用场景就是创建一个包裹函数, 传入所有的参数并返回接收到的所有值:

var obj = {
    name: 'muzi'
}v

function foo() {
    console.log(this.name)
}

function wrapper() {
    foo.call(obj)
}

wrapper()  // muzi
wrapper.call({ name: 'yaya' })  // muzi

虽然 wrapper 绑定了一个新的对象, 但当 wrapper 每次被调用时,
foo 都会显示绑定 obj 对象;

所以无论 wrapper 如何调用, foo 函数的绑定值都不会被改变.

我们可以根据这个特性, 创建一个辅助绑定函数:

function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments)
  }
}
function foo(b, c) {
    return this.a + b + c
}

var bar = bind(foo, { a: 1 })

bar(2, 3) // 6
bar.call({ a: 10 }, 2, 3) // 6

事实上, 早在 ES5 就提供了原生的硬绑定方法: Function.prototype.bind

var bar = foo.bind({ a: 1 })

bar(2, 3)  // 6
bar.call({ a: 10 }, 2, 3)  // 6

Function.prototype.bind 的实现:

Function.prototype._bind = function(obj) {
  var self = this
  return function() {
    return self.apply(obj, arguments)
  }
}

new绑定

function foo(a) {
	this.a = a; 
}
var bar = new foo(2); 
console.log( bar.a ); // 2 

使用 new 来调用 foo(…) 时,我们会构造一个新对象并把它绑定到 foo(…) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

优先级

new > 显式绑定 > 隐式绑定 > 默认绑定

ES6 箭头函数

箭头函数不使用 this 的四种标准规则, 而是根据外层(函数或者全局)作用域来决定 this

function foo() { 
    // 返回一个箭头函数
	return (a) => { 
        //this 继承自 foo() 
        console.log( this.a ); 
    }; 
}
var obj1 = { 
    a:2 
};
var obj2 = { 
    a:3 
};
var bar = foo.call( obj1 ); 
bar.call( obj2 ); // 2, 不是 3 !

foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() { 
    setTimeout(() => { // 这里的 this 在此法上继承自 foo() 
        console.log( this.a ); 
    },100); 
}
var obj = { a:2 };
foo.call( obj ); // 2

箭头函数可以像 bind(…) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。

function foo() { // foo()作用域
	var self = this; // 普通函数作为回调函数,由于函数作用域的不同,会发生this隐式丢失
    setTimeout( function(){ //内部函数作用域
        console.log( self.a ); 
    }, 100 ); 
}
var obj = { a: 2 };
foo.call( obj ); // 2

小结

如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置(当前执行的函数的前一个调用中)。找到之后就可以顺序应用下面这五条规则来判断 this 的绑定对象。

  1. 由 new 调用?绑定到新创建的对象。

  2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。

  3. 由上下文对象调用?绑定到那个上下文对象。

  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象。

  5. 箭头函数的this? 由外层作用域的this来决定

更新一个比较绕的关于this的读程题,运行一下看结果,按照上面对this指向的解读仔细想一下

            const obj = {
                    dev: 'bfe',
                    a: function() {
                        return this.dev
                    },
                    b() {
                        return this.dev
                    },
                    c: () => {
                        return this.dev
                    },
                    d: function() {
                        return (() => {
                            return this.dev
                        })()
                    },
                    e: function() {
                        return this.b()
                    },
                    f: function() {
                        return this.b
                    },
                    g: function() {
                        return this.c()
                    },
                    h: function() {
                        return this.c
                    },
                    i: function() {
                        return () => {
                            return this.dev
                        }
                    }
                }
                console.log( obj.a() )
                console.log( obj.b() )
                console.log( obj.c() )
                console.log( obj.d() )
                console.log( obj.e() )
                console.log( obj.f()() )  // obj.f() 得到 this.b,这里其实就是拿到了 b方法的引用,跟this没关系了
                console.log( obj.g() )
                console.log( obj.h()() )
                console.log( obj.i()() )

参考文献

你不知道的js上

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值