this--浅析


从开始接触this到真正理解this之前,可能会对this有一些误解, 如:

  1. 第一种常见的倾向是认为this指向函数自己;

  2. 第二常见的对this的含义的误解,是它不知怎的指向了函数的作用域。

明确地说,this不会以任何方式指向函数的 词法作用域。作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说是对的。但是JavasScript代码不能访问作用域“对象”。它是 引擎 的内部实现。

什么是this?

上面已经列举了各种不正确的臆想,现在来看this机制是到底如何真正工作的。

我们早先说过,this不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this绑定和函数声明的位置无关,反而和函数被调用的方式有关。

当一个函数被调用时,会建立一个活动记录,也称为执行环境。这个记录包含函数是从何处(call-stack)被调用的,函数是 如何 被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。

简单来说, 大概有4种:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new绑定
    特别要注意隐式赋值,比如传参 、赋值的时候,实际上对象和函数是两个地址, 如果不加上对象的调用,单独调用实际上就是在全局环境调用函数。下面一一道来。

1. 默认绑定

最常见的情况:独立函数调用。可以认为这种this规则是在没有其他规则适用时的默认规则。
考虑如下代码:

function foo() {
    console.log(this.a);
}
var a = 1;  // 全局变量
foo();  // 1

当foo()被调用时,this.a解析的是全局变量a。为什么?因为在这种情况下,foo()是在全局环境下被调用的,对此方法调用的this实施了 默认绑定,所以使this指向了全局对象。

如果在严格模式下,那么对于 默认绑定 来说全局对象是不合法的,所以this将被设置为undefined。

function foo() {
    "use strict";
    console.log(this.a);
}
var a = 1;  // 全局变量
foo();  // TypeError: 'this' is undefined

2. 隐式绑定(Implicit Binding)

另一种要考虑的规则是:调用函数的是一个环境对象(context object)。
考虑如下代码:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 1,
    foo: foo;
};
obj.foo();  // 1

首先,注意foo()被声明然后作为引用属性添加到obj上的方式。无论foo()是否一开始就在obj上被声明,还是后来作为引用添加(如上面代码所示),都是这个 函数 被obj所“拥有”或“包含”。

只有对象属性引用链的最后一层是影响调用点的。比如:

function foo() {
    console.log(this.a);
}
var obj1 = {
    a: 1,
    foo: foo;
};
var obj2 = {
    a: 2,
    obj1: obj1;
};
obj2.obj1.foo();  // 1
隐式丢失(Implicitly Lost)

this绑定最常让人沮丧的事情之一,就是当一个 隐含绑定 丢失了它的绑定,这通常意味着它会退回到 默认绑定, 根据strict mode的状态,结果不是全局对象就是undefined。

考虑如下代码:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 1,
    foo: foo;
};
var a = 'global';
var fn = obj.foo;  // 将函数引用复制给fn
fn();  // 'global'

尽管fn似乎是obj.foo的引用,但实际上它只是另一个foo自己的引用而已。另外,起作用的调用位置是fn(),一个直白,毫无修饰的调用,因此 默认绑定 适用于这里。

这种情况发生的更加微妙,更常见,更意外的方式,是当我们考虑传递一个回调函数时:

function foo() {
    console.log(this.a);
}
function foo2(fn) {
// 这里 fn 仅仅是 foo 的一个 引用
    fn();    // 调用位置
}
var obj = {
    a: 1,
    foo: foo;
};
var a = 'global';
var fn = obj.foo;  // 将函数引用复制给fn
foo2(obj.foo);  // 'global'

js里参数都是按值传递的, 所以在执行foo2(obj.foo)的时候,所以obj.foo将foo的引用值传给了fn,之后运行回调函数fn(), 结果跟上一个例子一样。

那么如果接收你所传递回调的函数不是你的,而是语言内建的呢?没有区别,同样的结果。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 1,
    foo: foo;
};
var a = 'global';
setTimeout(obj.foo, 1000) // 'global'

如我们刚刚看到的,我们的回调函数丢掉他们的this绑定是十分常见的事情。

不管哪一种意外改变this的方式,你都不能真正地控制你的回调函数引用将如何被执行,所以你(还)没有办法控制调用点给你一个故意的绑定。我们很快就会看到一个方法,通过 固定 this来解决这个问题。


显示绑定(Explicit Binding)

JavaScript语言中的“所有”函数都有一些工具(通过他们的[[Prototype]])可以用于这个任务。特别是,函数拥有call(..)和apply(..)方法。绝大多数被提供的函数,当然还有你将创建的所有的函数,都可以访问call(..)和apply(..)。

call(..)和apply(..)接收的第一个参数都是一个用于绑定this的对象,之后使用这个指定的this来调用函数。因为你已经直接指明你想让this是什么,所以我们称这种方式为 显示绑定(explicit binding)。

考虑如下代码:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 1
};
var a = 'global';
foo.call(obj);  // 1

通过foo.call(..)使用 明确绑定 来调用foo,允许我们强制函数的this指向obj。

如果你传递一个简单原始类型值(string,boolean,或 number类型)作为this绑定,那么这个原始类型值会被包装在它的对象类型中(分别是new String(..),new Boolean(..),或new Number(..))。这通常称为“boxing(封箱)”。

注: 就this绑定的角度讲,call(..)和apply(..)是完全一样的。它们确实在处理其他参数上的方式不同。

不幸的是,单独依靠 明确绑定 仍然不能为我们先前提到的问题提供解决方案,也就是函数“丢失”自己原本的this绑定,或者被第三方框架覆盖,等等问题。

硬绑定(Hard Binding)

考虑这段代码:

function foo(sth) {
    console.log(this.a, sth);
    return this.a + sth;
}
var obj = {
    a: 1;
}
var bar = function() {
    return foo.apply(obj);
};
bar();   // 1
setTimeout(bar, 100);  // 1
// bar 将 foo的this应绑定到 obj
bar.call(window);  // 1

代码中, 创建了一个函数bar(),在它的内部手动调用foo.call(obj),由此强制this绑定到obj并调用foo。无论你过后怎样调用函数bar,它总是会使用obj调用foo。这种绑定即明确又坚定,所以我们称之为 硬绑定(hard binding)。

用 硬绑定 将一个函数包装起来的最典型的方法,是为所有传入的参数和传出的返回值创建一个通道:

function foo(sth) {
    console.log(this.a, sth);
    return this.a + sth;
}
var obj = {
    a: 1;
}
var bar = function() {
    return foo.apply(obj, arguments);
};
var baz = bar(2);   // 1 2
console.log(baz);  // 3

另一种表达这种模式的方法是创建一个可复用的帮助函数:

function foo(sth) {
    console.log(this.a, sth);
    return this.a + sth;
}
function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    };
}
var obj = {
    a: 1;
}
var bar = bind(foo, obj);
var baz = bar(2);   // 1 2
console.log(baz);  // 3

硬绑定 是一个很常用的模式,它已作为ES5的内建工具提供:Function.prototype.bind,像这样使用:

function foo(sth) {
    console.log(this.a, sth);
    return this.a + sth;
}
var obj = {
    a: 1;
}
var bar = foo.bind(obj);
var baz = bar(2);   // 1 2
console.log(baz);  // 3

bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用函数。


new绑定(new Binding)
  • 一个全新的对象会凭空创建(就是被构建)
  • 这个新构建的对象会被接入原形链([[Prototype]]-linked)
  • 这个新构建的对象被设置为函数调用的this绑定
  • 除非函数返回一个它自己的其他 对象,这个被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 binding)。


总结: this绑定基本有4中可能:
1. 默认绑定,指向顶层对象,在严格模式下, 指向undefined,混杂模式下指向全局对象;
2. 隐式绑定,考虑调用位置是否有上下文,或者说是否被某个而对象拥有或者包含,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。此时,存在的一个最大的问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定。
3. 显示绑定,call、apply、bind 绑定,将传给他们的第一个参数作为绑定this的对象。(硬绑定会通过Function.prototype.bind()创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上)。
4. new绑定,this指向新创建的实例对象。

bind的简单实现:

function bind(fn, obj){
    return function() {
        fn.apply(obj, arguments);
    };
}

上述四种绑定的优先级:
1. 默认绑定的优先级最低。
2. 显示绑定高于隐式绑定;
3. new绑定高于显示绑定;
所以有显示 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定 。


以上是学习总结,请多指正!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值