程序员

你不知道的JavaScript(二)|this和对象原型

2017-09-29  本文已影响28人  xpwei

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 的调用位置

绑定规则

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

在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此this会绑定到undefined:

function foo() {
    "use strict";
    console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined

这里有一个微妙但是非常重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;严格模式下与foo()的调用位置无关:

function foo() {
    console.log(this.a);
}
var a = 2;
(function () {
    "use strict";
    foo(); // 2
})();
function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

function foo() {
    console.log(this.a);
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

function foo() {
    console.log(this.a);
}
function doFoo(fn) {
    // fn 其实引用的是foo
    fn(); // <-- 调用位置!
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo(obj.foo); // "oops, global"

显示绑定
用call和apply

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

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者new Number(..))。这通常被称为“装箱”。
显示绑定仍然无法解决我们之前提出的丢失绑定问题。但是显示绑定的一个变种可以解决这个问题。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2
};
var bar = function () {
    foo.call(obj);
};
bar(); // 2
setTimeout(bar, 100); // 2
// 硬绑定的bar 不可能再修改它的this
bar.call(window); // 2

我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显示的强制绑定,因此我们称之为硬绑定。
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接受到的所有值:

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

另一种使用方法是创建一个可以重复使用的辅助函数:

function foo(something) {
    console.log(this.a, something);
    return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
    return function () {
        return fn.apply(obj, arguments);
    };
}
var obj = {
    a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); // 5

由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,它的用法如下:

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

bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数。

function foo(el) {
    console.log(el, this.id);
}
var obj = {
    id: "awesome"
};
// 调用foo(..) 时把this 绑定到obj
[1, 2, 3].forEach(foo, obj);
    // 1 awesome 2 awesome 3 awesome

这些函数实际上就是通过call(..)或者apply(..)实现了显式绑定,这样可以少写一些代码。

new绑定
包括内置对象函数(比如Number(..))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new来调用函数,或者说发生构造函数低啊用时,会自动执行下面的操作:

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

以上代码使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。

优先级
显示绑定优先级高于隐式绑定
new绑定比隐式绑定优先级高
new绑定比硬绑定高
为什么要在new中使用硬绑定函数呢?直接使用普通函数不是更简单吗?
之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”):

function foo(p1, p2) {
    this.val = p1 + p2;
}
// 之所以使用null 是因为在本例中我们并不关心硬绑定的this 是什么
// 反正使用new 时this 会被修改
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
baz.val; // p1p2

判断this
根据优先级来判断函数在某个调用位置应用的是哪条规则:
1、函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
2、 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。var bar = foo.call(obj2)
3、函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo()
4、如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。var bar = foo()

被忽略的this
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:

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

什么情况下会传入null呢?一种非常常见的做法是使用apply(..)来“展开”一个数组,并当做参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:

function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 把数组“展开”成参数
foo.apply(null, [2, 3]); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind(null, 2);
bar(3); // a:2, b:3

这两种方法都需要传入一个参数当做this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择,就像代码所示的那样。
在ES6中,可以用...操作符代替apply(..)来“展开”数组,foo(..[1,2])和foo(1,2)是一样的,这样可以避免不必要的this绑定。可惜,在ES6中没有柯里化的相关语法,因此还是需要使用bind(..)。

更安全的this
使用null来忽略this绑定可能产生一些副作用。一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用。在JavaScript中创建一个空对象最简单的方法都是Object.create(null)。Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}"更空":

function foo(a, b) {
    console.log("a:" + a + ", b:" + b);
}
// 我们的DMZ 空对象
var ø = Object.create(null);
// 把数组展开成参数
foo.apply(ø, [2, 3]); // a:2, b:3
// 使用bind(..) 进行柯里化
var bar = foo.bind(ø, 2);
bar(3); // a:2, b:3

间接引用
另一个需要注意的是,你有可能会创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

function foo() {
    console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2

赋值表达式p.foo=0.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者0.foo()。根据我们之前说过的,这里会应用默认绑定。

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
雨下了走好路,这句话我记住
上一篇下一篇

猜你喜欢

热点阅读