《你不知道的JavaScript》-- 精读(七)

知识点

1.this全面解析

1.1 调用位置

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

一般来讲,寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的全部函数)。咱们关心的调用位置就在当前正在执行的函数的前一个调用中。java

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.2 绑定规则

咱们来看看在函数的执行过程当中调用位置如何决定this的绑定对象。安全

必须找到调用位置,而后判断须要应用下面规则中的哪一条。app

1.2.1 默认绑定

首先要介绍的是最经常使用的函数调用类型:独立函数调用。能够把这条规则看做是没法应用其余规则时的默认规则。函数

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

咱们知道,声明在全局做用域中的变量(好比var a = 2;)就是全局对象的一个同名属性。当调用foo()时,this.a被解析成了全局变量a。由于在本例中,函数调用时应用了this的默认绑定,所以this指向全局对象。oop

如何判断默认绑定呢?能够经过分析调用位置来看foo()是如何调用的。在代码中,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()则不影响默认绑定:ui

function foo() {
    console.log(this.a);
}
var a = 2;
(function(){
 "use strict";
    foo(); // 2
})()
复制代码

1.2.2 隐式绑定

另外一条须要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或包含,不过这种说法可能会形成一些误导。

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
obj.foo(); // 2
复制代码

当foo()被调用时,它的前面确实加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的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"
复制代码

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

若是把函数传入语言内置的函数而不是传入你本身声明的函数,会发生什么呢?结果是同样的,没有区别:

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
var a = "oops,global"; // a是全局对象的属性
setTimeout(obj.foo,1000); // "oops,global"
复制代码

JavaScript环境中内置的setTimeout()函数实现和下面的伪代码相似:

function setTimeout(fn,delay){
    // 等待delay毫秒
    fn(); // 调用位置
}
复制代码

如上,回调函数丢失this绑定是很是常见的。除此以外,调用回调函数的函数也可能会修改this。

不管哪一种状况,this的改变都是意想不到的,可是咱们能够经过固定this来修复这个问题。

1.2.3 显式绑定

分析隐式绑定时,咱们必须在一个对象内部包含一个指向函数的属性,并经过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。

那么若是咱们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么作呢?

可使用函数的call(..)和apply(..)方法。它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。由于你能够直接指定this的绑定对象,所以咱们称之为显式绑定。

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的上下文并调用原始函数。

API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,一般被称为“上下文”(context),其做用和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(..)实现了显式绑定。

1.2.4 new绑定

包括内置对象函数在内的全部函数均可以用new来调用,这种函数调用被称为构造函数调用。实际上,并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

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

  • 1.建立(或者说构造)一个全新的对象
  • 2.这个新对象会被执行[[Prototype]]链接
  • 3.这个新对象会绑定到函数调用的this
  • 4.若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
复制代码

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

1.3 优先级

如今咱们知道了函数调用中this绑定的四条规则,接下来介绍的是这些规则的优先级。

毫无疑问,默认绑定的优先级是四条规则中最低的,隐式绑定和显式绑定哪一个优先级更高?咱们来测试一下:

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

obj1.foo.call(obj2); // 3;
obj2.foo.call(obj1); // 2
复制代码

能够看到,显式绑定的优先级更高。

接下来咱们看一下new绑定和隐式绑定的优先级谁高谁低:

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

obj1.foo.call(obj2,3); 
console.log(obj2.a); // 3

var bar = new obj1.foo(4);
console.log(obj1.a); // undefined
console.log(bar.a); // 4
复制代码

能够看到,new绑定比隐式绑定优先级高,可是new绑定和显式绑定谁的优先级更高呢?

function foo(p1,p2) {
    this.val = p1 + p2
}
var bar = foo.bind(null,"p1");
var baz = new bar("p2");
baz.val; // p1p2
复制代码

能够看到,new绑定比显式绑定的优先级高。

判断this

如今咱们能够根据优先级来判断函数在某个调用位置应用的是哪条规则。

  • 1.函数是否在new中调用(new绑定)?若是是的话this绑定的是新建立的对象。

    var bar = new foo()

  • 2.函数是否经过call、apply(显式绑定)或者硬绑定调用?若是是的话,this绑定的是指定的对象。

    var bar = foo.call(obj2)

  • 3.函数是否在某个上下文对象中调用(隐式绑定)?若是是的话,this绑定的是那个上下文对象

    var bar = obj1.foo()

  • 4.若是都不是的话,使用默认绑定。若是在严格模式下,就绑定到undefined,不然绑定到全局对象。

    var bar = foo()

1.4 绑定例外

1.4.1 被忽略的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多是一个不错的选择。

然而老是使用null来忽略this绑定可能产生一些反作用。由于默认绑定规则会将this绑定到全局对象。

更安全的this

一种“更安全”的作法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何反作用。

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
复制代码

1.4.2 间接引用

另外一个须要注意的是,你有可能(有意或者无心地)建立一个函数的“间接引用”,在这种状况下,调用这个函数会应用默认绑定规则。

间接引用最容易在赋值时发生:

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绑定对象的并非调用位置是否处于严格模式,而是函数体是否处于严格模式。若是函数体处于严格模式,this会被绑定到undefined,不然this会被绑定到全局对象。

1.5 this词法

箭头函数并非使用function关键字定义的,而是使用被称为“胖箭头”的操做符=>定义的。箭头函数不使用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() {
    var self = this;
    setTimeout(function() {
        console.log(self.a);
    },100)
}

var obj = {
    a: 2
}

foo.call(obj); // 2
复制代码

虽然self = this和箭头函数看起来均可以取代bind(...),可是从本质上来讲,它们想替代的是this机制。

若是你常常编写this风格的代码,可是绝大部分时候都会使用self = this或者箭头函数来否认this机制,那你或许应当:

  • 1.只使用词法做用域并彻底抛弃错误this风格的代码;
  • 2.彻底采用this风格,在必要时使用bind(..),尽可能避免使用self = this和箭头函数。

总结

若是要判断一个运行中函数的this绑定,就须要找到这个函数的直接调用位置。找到以后,就能够顺序应用下面这四条规则来判断this的绑定对象。

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

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

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

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

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法做用域来决定this,具体来讲,箭头函数会继承外层函数调用的this绑定(不管this绑定到什么)。

巴拉巴拉

好像有近一个月没有更新了,感受本身是懒癌犯了,不想推脱说工做忙,或者太累,由于老是有时间刷豆瓣,刷知乎,最近发现,不少时候,被指责,被批评,第一反应是找借口开脱,不知道是否是只有我一我的这样,因此开始学会从自身找缘由,你作的好,确定不会被批,固然,也许会有特地找茬的状况,但是,我身边都是很好的人,不存在这样的状况,须要学习和提高的地方好多,只能慢慢来啦,我看这本书的时候,有不少地方会理不顺,就随手去翻了一下《JavaScript权威指南》,发现写的真好啊,值得细读,推荐给你们~。

相关文章
相关标签/搜索