【JavaScript】this - 笔记

this

关于this,其实this是提供了一种更优雅的方式来隐式“传递”一个对象引用。灵活的应用this,可让咱们在写内容较多的代码时,不会显得愈来愈混乱。bash

关于this

指向自身

根据英语的语法角度来推断的话,this能够理解为指向函数自身。可是在实际中并非如此。app

function foo(num) {
    console.log( "foo: " + num );
    // 记录 foo 被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9


console.log( foo.count );// 0

console.log(count); //NaN
复制代码

上述代码中,咱们企图用this.count指向foo函数自己,以此在其做用域建立一个count属性。可是是代码的实际结果告诉咱们,这样写并不能作到咱们所想要的结果。函数

这种写法,最后会使代码在无心中建立了一个全局变量count,其值为NaNoop

若是要从函数对象内部引用其自身,只使用this是不够的。通常而言,须要一个指向函数对象的词法标识符来引用它。ui

对于上述状况,其实有许多解决方式。可是既然咱们的主题是this,那就用this的方式来解决。this

function foo(num) {
    console.log( "foo: " + num );
    // 记录 foo 被调用的次数
    // 注意,在当前的调用方式下(参见下方代码),this 确实指向 foo
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        // 使用 call(..) 能够确保 this 指向函数对象 foo 自己
        foo.call( foo, i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

console.log( foo.count ); // 4
复制代码

这个方式,是强制this指向foo函数对象。编码

this的做用域

对于this,还有一种误解。就是this指向函数的做用域。spa

可是实际上,this在任何状况下都不指向函数的词法做用域。prototype

举个例子:code

function foo() {
    var a = 2;
    this.bar();
}
function bar() {
    console.log( this.a );
}
foo(); // ReferenceError: a is not defined
复制代码

上述方法,其实在有必定代码基础的人眼里,会以为很奇怪。由于咱们要调用bar()方法的话,这个this其实是多余的。

而且,this这个操做并不能跨做用域引用变量。

切记,不要将this和词法做用域混合使用。

What is 'this'

在标题上玩了一下英语的谐音梗,哈哈哈,但愿不要介意。

上面讲述了那些以后,咱们须要了解到的是,this究竟是一个什么样的机制。

其实,this的绑定和函数声明的位置,没有半毛钱关系,半分钱也没有。它只取决于函数的调用方式。

当一个函数被调用时,会建立一个执行上下文。它会包含函数在哪里被调用、调用方法和传参等信息。this就是其中的一个属性,在函数执行过程当中会用到。

this的全面解析

绑定规则

在了解this的绑定规则以前,咱们须要知道了解调用位置。

调用位置实际上就是指咱们调用对象的词法标识符的位置。

this的绑定规则有四条。

默认绑定

最经常使用的函数调用类型:独立函数调用。这种方法就可使用默认绑定。

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

var a = 2;
 foo(); //2
复制代码

咱们能够发现,咱们在调用foo()时,其内部的this指向了全局对象,这是由于应用了this的默认绑定。

代码中,foo()是直接使用不带任何修饰的函数引用进行调用,所以只能使用默认绑定,没法应用其余规则。

须要注意的是,上述状况只有在非严格模式下 使用。

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

隐式绑定

隐式的使用,须要考虑调用位置是否有上下文对象。

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

var obj = {
    a:2,
    foo:foo
};
obj.foo();
复制代码

咱们在使用了obj.foo()的方式来调用函数时,函数foo()其实不属于obj对象。可是这种方式,让函数的落脚点在obj对象内,this指向的是obj对象内,所以能够调用到obj内部的变量。

隐式丢失 在使用this绑定的时候,有个常见的问题,就是被隐式绑定的函数会丢失绑定对象。

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,可是实际上,引用的仍是函数自己。因此,此时 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"
复制代码

咱们在使用this的时候,应该严格注意避开相似状况。由于咱们没法控制回调函数的执行方式,从而致使没法正确的使用咱们想要使用的绑定方式。

显式绑定

JavaScript 中的“全部”函数都有一些有用的特性,能够用来解决这个问题。具体点说,可使用函数的 call(..)apply(..) 方法。严格来讲,JavaScript 的宿主环境有时会提供一些很是特殊的函数,它们并无这两个方法。可是这样的函数很是罕见,JavaScript 提供的绝大多数函数以及你本身建立的全部函数均可以使用 call(..)apply(..) 方法。

显示绑定主要分为两种。

  1. 硬绑定 硬绑定其实就是使用 call(..)apply(..) 方法。

    call

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

    apply:

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

因为硬绑定是一种很是经常使用的模式,因此在 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 的上下文并调用原始函数。

  1. API调用的上下文

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,一般被称为“上下文”(context),其做用和 bind(..) 同样,确保你的回调函数使用指定的 this

实际上该种方法也是经过call()或者apply()来实现的显式绑定,只不过不须要咱们本身来写。

new绑定

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用 new 初始化类时会 调用类中的构造函数。一般的形式是这样的:

something = new MyClass(..);
复制代码

JavaScript 也有一个 new 操做符,使用方法看起来也和那些面向类的语言同样,绝大多数开发者都认为 JavaScriptnew 的机制也和那些语言同样。然而,JavaScriptnew 的机制实际上和面向类的语言彻底不一样。

实际上, 在JavaScript中并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

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

  1. 建立(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 链接。
  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绑定。

优先级

上述说了四种绑定方式,在实际开发中,确定会遇到多种绑定交叉的状况。那咱们就须要知道,绑定的优先级如何,才能够预知到代码结果。

判断this

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

  1. 函数是否在 new 中调用( new 绑定)?若是是的话 this 绑定的是新建立的对象。
var bar = new foo()
复制代码
  1. 函数是否经过 callapply (显式绑定)或者硬绑定调用?若是是的话, this 绑定的是指定的对象。
var bar = foo.call(obj2)
复制代码
  1. 函数是否在某个上下文对象中调用(隐式绑定)?若是是的话, this 绑定的是那个上 下文对象。
var bar = obj1.foo()
复制代码
  1. 若是都不是的话,使用默认绑定。若是在严格模式下,就绑定到 undefined ,不然绑定到全局对象。
var bar = foo()
复制代码

就是这样。对于正常的函数调用来讲,理解了这些知识你就能够明白 this 的绑定原理了。

绑定例外

凡是规则,都会有出现意外的状况。this绑定也不例外。

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

当咱们把null或者undefined传入callapply或者bind时,这些值在调用时会被忽略,从而去应用默认绑定规则。

间接引用

当咱们建立一个函数的“间接引用”时,调用这个函数就会应用默认绑定规则。

简介引用最容易在赋值时发生。

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 会被绑定到全局对象。

软绑定

前面有提到一种绑定方式,叫作硬绑定,因此通常相对的,其实也有一种叫作软绑定的方式。

使用硬绑定会下降函数的灵活性,以后没法再使用隐式绑定或者显式绑定来修改this。可是软绑定 ,能够保留隐式绑定和显示绑定修改this的能力。

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获全部 curried 参数
        var curried = [].slice.call( arguments, 1 );
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                    obj : this
                curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}
复制代码

this词法

ES6中,介绍了一种特殊的函数类型:箭头函数。

箭头函数并非使用 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 绑定到 obj1bar (引用箭头函数)的 this 也会绑定到 obj1 ,箭头函数的绑定没法被修改。

箭头函数能够像 bind(..) 同样确保函数的 this 被绑定到指定对象,此外,其重要性还体 如今它用更常见的词法做用域取代了传统的 this 机制。

总结

要判断this绑定,就须要先找到函数的直接调用位置。

以后根据规则来判断this的绑定对象:

1. `new`调用
复制代码

2.callapply或者bind 3.上下文对象调用 4.默认绑定

ES6中的箭头函数并不会使用四条标准的绑定规则。而是根据当前的词法做用域来决定this箭头函数会继承外层函数调用的 this 绑定

相关文章
相关标签/搜索