this全面解析

177 阅读8分钟

this是在运行时绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

1. 调用位置

调用位置是函数被调用的位置(不是声明的位置),只有仔细分析调用位置才能知道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的调用位置
  • 开发者模式中设置断点,或者代码中插入debugger;调试器在那个位置暂停,同时会展示当前位置的函数调用列表,这就是调用栈。

2. 绑定规则

2.1 默认绑定

独立函数调用:无法应用其他规则时的默认规则。

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

this.a被解析成了全局变量a,函数调用时应用了this的默认绑定,因此this指向全局变量。

严格模式下,全局对象无法使用默认绑定,this会绑定到undefined。

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

// --------------------------------------

// 严格模式下调用函数不影响默认绑定。
function foo(){
    console.log(this.a);
}
var a = 2;
(function(){
    'use strict';
    foo(); // 2;
});

2.2 隐式绑定

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

当foo被调用后,this绑定到这个上下文对象obj上。

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

var obj = {
    a: 2,
    foo: foo
}
obj.foo(); // 2

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

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

var obj1 = {
    a: 2,
    obj2: obj2
}

var obj2 = {
    a: 3,
    foo: foo
}
obj.foo(); // 3

隐式丢失,被隐式绑定的函数会丢失绑定对象,它会应用默认绑定,从而把this绑定到全局对象上或者undefined(取决于是否严格模式)

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

var obj = {
    a: 2,
    foo: foo
} 

var bar = obj.foo; // 函数别名
var a = "oops, globel!" // a是全局对象的属性
bar(); // oops, globel!

虽然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, globel!" // a是全局对象的属性
doFoo(obj.foo) // oops, globel!

/**
* 把函数传入内置的函数而不是自己声明的函数也是一个结果
*/
function foo(){
    console.log(this.a);
}

var obj = {
    a: 2,
    foo: foo
} 

var a = "oops, globel!" // a是全局对象的属性
setTimeout(obj.foo, 100) // oops, globel!

参数传递就是一种隐式赋值,所以传入函数也会被隐式赋值。回调函数丢失this是非常常见的。

2.3 显式绑定

通过call(...)apply(...)方法,第一个参数是this绑定的对象。因此,直接指定this的绑定对象,称之为显式绑定。

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

var obj = {
    a: 2
}

foo.call(obj); // 2

但显式绑定无法解决之前提出的绑定丢失问题

  • 硬绑定

创建函数bar,在函数内部调用foo.call(obj),强制把foo的this指向obj;

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

var obj = {
    a: 2
}

var bar = function() {
    foo.call(obj);
}

bar(); // 2
setTimeout(bar, 100) // 2

// 硬绑定不可能在修改它的this
bar.call( window ); // 2

典型的应用场景是创建一个包裹函数,负责接收参数并返回值

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,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
  • API调用的上下文

javascript许多内置的函数提供了一个可选参数,被称之为“上下文(context)”,作用和bind(...)一样,确保回调函数使用指定的this,

function foo(el) {
	console.log( el, this.id );
}

var obj = {
    id: "awesome"
}

var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

2.4 new 绑定

  • 构造函数只是使用new操作符时被调用的普通函数。
  • 包括内置对象函数(比如Number(...))在内的所有函数d都可以用new来调用,这种函数称为构造函数借用。
  • 实际上并不存在所谓的‘构造函数’,只有对函数的‘构造调用’。

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

  1. 创建(构造)一个全新的对象。
  2. 这个新对象会被执行[[prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

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

function foo(a) {
    this.a = a;
}

var obj = new foo(2);
console.log(obj.a); // 2

3. 优先级

this优先级顺序

  1. 函数是否在new中调用(new绑定),如果是的话,this绑定的是新创建的对象。
  2. 函数是否通过callapply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
  3. 函数是否在某个上下文对象调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

4. 绑定例外

4.1 被忽略的this

如果你把null或者undefined作为this的绑定对象传入到callapply或者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: 2

// ---------------------------
// 使用bind(..)进行柯里化
var bar = foo.bind(null, 2)
bar(3); // a: 2 b: 3

总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。

更安全的this

创建一个空对象最简单的方法是Object.create(null),这个和{}很像,但并不会创建Object.prototype委托,把这个对象绑定到你的程序不会产生任何副作用。

function foo(a, b) {
    console.log("a: " + a + " b: " + b);
}

// 空对象
var ø = Object.create(null);

// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3

// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3 

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();所以这里应用默认绑定。

4.3 软绑定

  • 硬绑定可以强制把this绑定到指定的对象上(new除外),防止函数调用应用默认规则。但是会降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或者显式绑定来修改this
  • 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到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;
    };
}

使用:软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj。

function foo() {
    console.log("name:" + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 

// 隐式绑定规则
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 看!!!

// 显式绑定规则
fooOBJ.call( obj3 ); // name: obj3 <---- 看!!!

// 绑定丢失,应用软绑定
setTimeout( obj2.foo, 10 ); // name: obj

5. this词法

ES6中新增了一种无法使用规则的特殊函数类型:箭头函数。 箭头函数的词法作用域:

function foo() {
    return (a) => {
        // this继承自foo()
        console.log(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 也不行)。

ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。

function foo() {
    var self = this; // lexical capture of this
    setTimeout( function() {
        console.log( self.a ); // self只是继承了foo()函数的this绑定
    }, 100 );
}

var obj = {
    a: 2
};

foo.call(obj); // 2
  • 只使用词法作用域并完全抛弃错误this风格的代码;
  • 完全采用this风格,在必要时使用bind(..),尽量避免使用 self = this 和箭头函数