JavaScript学习笔记5--this全面解析

  |  

前言

继续关于《你不知到的JavaScript》的学习,今天来到第二部分的第一、二章,this全面解析


15



调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

我们来看一下下面这个示例程序并分析它的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
function baz() {
console.log("baz");
bar();
}
function bar() {
console.log("bar");
foo();
}
function foo() {
console.log("foo");
}

baz();

这个实例程序的调用栈很简单:window -> baz -> bar -> foo

绑定规则

当我们找到函数的调用位置后,将会执行this的绑定,this的绑定遵循四条规则

默认绑定

最常用的函数调用类型:独立函数调用。

默认绑定规则可以看作无法应用其它规则时的默认规则

通过分析调用位置来看,如果函数是不带任何修饰的函数引用进行调用的,就会进行默认绑定。但是需要注意的是,只有在非严格模式下,默认绑定才能绑定到全局对象下,否则全局对象无法使用默认绑定,this会绑定到undefined。

隐式绑定

另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包裹。

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。

对象属性链中只有最顶层或者说最后一层会影响调用位置

隐式丢失:

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定,最后根据是否采用严格模式来把this绑定到全局对象或者undefined上。

例:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a);
}

var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = "oops, global";
bar(); // "oops, global"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数的本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此采用了默认绑定。

一种更微妙、更常见并且更出人意料的情况发生在传入回调函数时:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn(); //调用位置
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global";
doFoo(obj.foo); //"oops, global"

参数传递其实是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以这个例子和上个例子是一样的结果。

显示绑定

就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函 数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。

JavaScript 中的“所有”函数都有一些有用的特性。具体点说,可以使用函数的 call(..) 和 apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们 并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我 们称之为显式绑定。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。

显示绑定仍然无法解决绑定丢失的问题,为了解决这个问题,我们采用硬绑定。

硬绑定:

例子:

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}

bar(); //2

我们创建了函数bar(),并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj。无论之后如何调用函数bar,它总会手动在obj上调用foo。这种绑定是一种显示的强制绑定,因此我们称之为硬绑定。

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

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

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

1
2
3
4
5
6
7
8
9
10
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的上下文并调用原始函数。

new 绑定

使用new来调用构造函数,或者说发生构造函数调用时,会自动执行下面的操作:

  • 创建一个全新的对象
  • 这个新对象会被执行[[原型]]连接
  • 这个新对象会绑定到函数调用的this
  • 如果函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象

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

优先级

我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:

  • 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()

  • 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)

  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()

  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo()

绑定例外

在某些场景下this的绑定会出乎意料,你认为应当应用其它绑定规则时,实际上应用的可能默认绑定规则。

被忽略的this

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

间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:

1
2
3
4
5
6
7
8
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 = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。根据我们之前说过的,这里会应用默认绑定.

this词法

我们之前介绍的四条规则已经可以包含所有正常的函数。但是 ES6 中介绍了一种无法使用 这些规则的特殊函数类型:箭头函数。
箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。

我们来看看箭头函数的词法作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

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

文章目录
  1. 1. 调用位置
  2. 2. 绑定规则
    1. 2.1. 默认绑定
    2. 2.2. 隐式绑定
  3. 3. 显示绑定
    1. 3.1. new 绑定
  4. 4. 优先级
  5. 5. 绑定例外
    1. 5.1. 被忽略的this
    2. 5.2. 间接引用
  6. 6. this词法