你不知道的JavaScript(上) - this

深入探讨JavaScript中this关键字的复杂机制,解释其绑定规则、优先级及例外情况,包括默认、隐式、显示和new绑定,以及箭头函数的this行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 关于this

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。

1.1 为什么要用this

function identify(){
    return this.name.toUpperCase();
}

function speak(){
    var greeting = `Hello, I'm ${identify.call(this)}`;
    return greeting;
}

var me = {
    name: 'Kyle'
}

var you ={
    name: 'Reader'
}

console.log(identify.call(me));  //KYLE
console.log(identify.call(you)); //READER

console.log(speak.call(me)); //Hello, I'm KYLE
console.log(speak.call(you)); //Hello, I'm READER

上面的代码可以在不同的上下文对象(me 和 you)中重复使用函数 identify() 和 speak(),不用针对每个对象编写不同版本的函数。如果不使用 this,则需要给 identify() 和 speak() 传入一个显式的上下文对象:

function identify(context){
    return context.name.toUpperCase();
}

function speak(context){
    var greeting = `Hello, I'm ${identify(context)}`;
    return greeting;
}

var me = {
    name: 'Kyle'
}

var you ={
    name: 'Reader'
}

console.log(identify(me));  //KYLE
console.log(identify(you)); //READER

console.log(speak(me)); //Hello, I'm KYLE
console.log(speak(you)); //Hello, I'm READER

随着使用模式越来越复杂,显式传递上下文对象会让代码变得越来越混乱,使用this则不会这样。this提供了一种跟优雅的方式隐式“传递”一个对象引用,可以将API设计得更加简洁并且易于复用。

1.2 关于this的错误认识

(1)将this理解成指向函数自身

function foo(num){
    console.log(`foo: ${num}`);
    this.count++;
}

foo.count = 0;

for (var i = 0; i<10; i++){
    if(i>5){
        foo(i);
    }
}

console.log(foo.count);

// foo: 6
// this.count: NaN
// foo: 7
// this.count: NaN
// foo: 8
// this.count: NaN
// foo: 9
// this.count: NaN
// 0

执行上述代码,会发现congsole.log()语句产生了4条输出,证明foo(..)确实被调用了4次,但foo.count的值仍然是0,显然foo()中的this并没有指向foo()自身,因此不能单纯从字面意思来理解this。事实上这段代码真正创建的是一个全局变量count,即this指向了全局对象,这个可在后续笔记中发现原理。

(2)将this理解成函数的作用域

将this理解成指向函数的作用域在某种情况下是正确的,但在其他情况下却是错误的,但需要明确的是,在任何情况情况下,this都不指向函数的词法作用域。在JavaScript内部,作用域确实和对象类似,可见的标识符都是它的属性,但是作用域"对象"无法通过JavaScript代码访问,它存在于JavaScript引擎内部。下面的代码试图使用this来隐式引用函数的词法作用域,但并没有成功:


function foo(){
    var a = 2;
    this.bar(); //此处调用成功纯属意外,最自然调用bar()的方法是省略前面的this,直接使用词法引用标识符
}

function bar(){
    console.log( this.a ); //此处试图使用this联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里的变量a,但这是不可能实现的,使用this不可能在词法作用域中查到什么
}

foo(); //undefined

每当想要把this和词法作用域的查找混合使用时,一定要谨记这是无法实现的。

1.3 this到底是什么

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

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

当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。

2. this全面解析

2.1 调用位置

在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置),只用仔细分析调用位置才能明确:这个this到底引用的是什么。

通常来说,寻找调用位置就是寻找“函数被调用的位置”,只是某些编程模式可能会隐藏真正的调用位置。最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数),所关心的调用位置就在当前正在执行的函数的前一个调用中。

调用栈和调用位置:


function baz(){
    //当前 调用栈:baz
    //因此当前 调用位置 是:全局作用域
    console.log("baz");
    bar();
}

function bar(){
    //当前 调用栈:baz -> bar
    //因此当前 调用位置 在 baz 中
    console.log("bar");
    foo();
}

function foo(){
    //当前 调用栈:baz -> bar -> foo
    //因此当前 调用位置 在 foo 中
    console.log("foo");
}

baz(); // <-- baz() 的调用位置


//baz
//bar
//foo

需注意是如何(从调用栈中)分析出真正的调用位置的,因为它决定了this的绑定。

2.2 绑定规则

以下显示的打印结果均为浏览器引擎环境下的结果,具有全局对象window。

2.2.1 默认绑定 - 独立函数调用时

独立函数调用是最常用的函数调用类型。默认绑定可以看作是无法应用其他规则时的默认规则。例:

/** 非严格模式 */
function foo() {
    console.log(this.a); //浏览器引擎环境下打印出 2,node环境下打印出 undefined
}
var a = 2;
foo();
/** 
 * foo():浏览器引擎环境下打印出 2,node环境下打印出 undefined,因为此处this为默认绑定全局对象,浏览器引擎环境下即为window。
 * 代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此this只能使用默认绑定,无法应用其他规则。
 * 声明在全局作用域中的变量(比如 var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
 */
/** 严格模式 */
function foo() {
    "use strict"; //foo()运行在strict mode下
    console.log(this);  //undefined
    console.log(this.a); //Uncaught TypeError: Cannot read property 'a' of undefined
}
var a = 2;
foo();
/**若使用严格模式(strict mode),则全局对象将无法使用默认绑定,因此this会绑定到undefined。
 * 虽然this的绑定规则完全取决于调用的位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局对象;严格模式下调用foo()则不影响默认绑定
 */
function foo() {
    console.log(this.a); //foo()运行在非strict mode下
}
var a = 2;
(function () {
    "use strict"; //在严格模式下调用foo()不影响默认绑定
    foo(); //浏览器引擎环境下打印出 2,node环境下打印出 undefined,
})(); 

注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。

2.2.2 隐式绑定 - 调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含[有时严格意义上不能说属于某个对象]

function foo(){
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
}
obj.foo(); //2
/**观察foo()的声明方式及其被如何当作属性添加到obj中,可以知道:无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
 * 但是,调用位置会使用obj上下文来引用函数,因此可以说函数被调用时是被obj对象“拥有”或者“包含”的。
 * 无论如何称呼这个模式,当foo()被调用时,其落脚点确实指向obj对象。
 * 即:当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
 * 因此调用foo()时this被绑定到obj上,此时 this.a 就等同于 obj.a
 */

但需要注意的是:对象属性引用链中,只有最顶层也可说最后一层会影响调用位置。例:

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

 var obj2 = {
     a: 42,
     foo: foo
 }

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

 obj1.obj2.foo(); // a: 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"
setTimeout(obj.foo, 100); //"oops, global"
/**无论是把函数传入自定义的函数还是语言内置的函数,结果都是一样的,本质上应用了默认绑定 */

如上所述,回调函数丢失this绑定是非常常见的,除此之外,还有一种情况:调用回调函数的函数可能会修改this,例如在一些流行的JavaScript库中时间处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。无论哪种情况,this的改变都是意想不到的,实际上无法控制回调函数的执行方式,也就无法控制会影响绑定的调用位置。可以通过固定this来修复这个问题。

2.2.3 显示绑定 - 不在对象内部包含函数引用,在某个对象上强制调用函数

隐式绑定时,必须在一个对象内部包含一个指向函数的属性,并通过这个属性简介引用函数,从而把this间接(隐式)绑定到这个对象上。若不想在对象内部包含函数引用,而想在某个对象上强制调用函数,则可利用 JavaScript 中的‘所有’函数都有的一些有用属性(与其原型有关)来解决这个问题:可以使用函数的call(...)和apply(...)方法。严格来说,JavaScript的宿主环境有时会提供一些非常特殊的函数,它们并没有这个两个方法,但是这样的函数非常罕见,JavaScript提供的绝大多数函数以及自定义的函数都可以使用call(...)和apply(...)方法。

call(...)和apply(...)方法工作原理:它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时制定这个this。从this绑定的角度来说,call(..)和apply(..)是一样的,它们的区别体现在其他的参数上。

因为这种方式可以直接制定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 

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

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(..)实现了显示绑定,这样可以少一些代码 */

2.2.4 new绑定

在传统的面向类的语言中,“构造函数”是类中的一种特殊方法,使用new初始化类时会调用类中的构造函数,通常形式是:someting = new MyClass(..)。JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,绝大多数开发者都认为JavaScript中new的机制也和那些语言一样。然而,JavaScript中new的机制实际上和面向类的语言完全不同。

在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。举例来说,ES5.1中这样描述 Number(..) 作为构造函数时的行为:

15.7.2 Number构造函数

当 Number 在 new 表达式中被调用时,它是一个构造函数:它会初始化新创建的对象。

所以,包括内置对象函数(比如Number(..))在内的所有函数都可以用new来调用,这种函数调用被称之为构造函数调用。需注意一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 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上 */

2.3 四条this绑定规则的优先级

上述已经了解了函数调用中 this 绑定的四条规则,需要做的就是找到函数的调用位 置并判断应当应用哪条规则。若某个调用位置可以应用多条规则,则需要给这些规则设定优先级。

2.3.1 隐式绑定与显示绑定优先级

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   显示绑定
/** 可明显看出:显示绑定优先级 高于 隐式绑定优先级,即在判断时应当先考虑是否可以应用显示绑定 */

2.3.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(obj1.a); //2  隐式绑定
 console.log(obj2.a); //3   显示绑定

 var bar = new obj1.foo(4);
 console.log(obj1.a); //2   隐式绑定
 console.log(bar.a); //4  new绑定

/**可以看出:new绑定优先级 高于 隐式绑定优先级 */

2.3.3 显示绑定与new绑定优先级

new 和 call/apply 无法一起使用,因此无法通过 new foo.call(obj1)来直接进行测试。但是可以使用硬绑定来测试它俩的优先级。首先回忆硬绑定的工作过程: Function.prototype.bind(..)会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。这样看起来硬绑定(也是显示绑定的一种)似乎比new绑定的优先级更高,无法使用new来控制this绑定。下面通过实例验证一下:

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); //2 
var baz = new bar(3);
console.log(obj1.a); //2 
console.log(baz.a); //3 

由例子看出,bar被硬绑定到obj1上,但是new bar(3)并没有像预计的那样把obj1.a修改为3。相反,new修改了硬绑定(到obj1的)调用bar(..)中的this。因为使用了new绑定,得到了一个名字为baz的新对象,并且baz.a的值为3。

看看之前介绍的“裸”辅助函数bind:

function bind(fn, obj) {
    return function () {
        fn.apply(obj, arguments);
    }
}

看起来在辅助函数中new操作符的调用无法修改this绑定,但是在上面的例子中new确实修改了this绑定。

实际上,ES5中内置的Function.prototype.bind(..)更加复杂。MDN提供了一种bind(..)实现,排版后如下:

if (!Function.prototype.bind) {
    Function.prototype.bind = function (oThis) {
        if (typeof this !== "function") { // 与 ECMAScript 5 最接近的 // 内部 IsCallable 函数
            throw new TypeError(
                "Function.prototype.bind - what is trying " +
                "to be bound is not callable");
        }
        var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this,
            fNOP = function () { }, fBound = function () {
                return fToBind.apply((
                    this instanceof fNOP &&
                        oThis ? this : oThis),
                    aArgs.concat(
                        Array.prototype.slice.call(arguments)
                    ));
            }
            ;
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP(); return fBound;
    };
}

这种 bind(..) 是一种 polyfill 代码(polyfill 就是我们常说的刮墙用的腻子,polyfill 代码主要用于旧浏览器的兼容,比如说在旧的浏览器中并没有内置 bind 函数,因此可以使用 polyfill 代码在旧浏览器中实现新的功能),对于 new 使用的硬绑定函数来说,这段 polyfill 代码和 ES5 内置的 bind(..) 函数并不完全相同(后面会介绍为什么要在 new 中使用硬绑定函 数)。由于 polyfill 并不是内置函数,所以无法创建一个不包含 .prototype 的函数,因此会具有一些副作用。如果你要在 new 中使用硬绑定函数并且依 赖 polyfill 代码的话,一定要非常小心。 

下面是new修改this的相关代码:

this instanceof fNOP && oThis ? this: oThis;
//...以及:
fNOP.prototype = this.prototype;
fBound.protptype = new fNOP();

简单来说,这段代码会判断硬绑定函数是否是被new调用,若果是的话就会使用新创建的this替换硬绑定的this。而之所以要在new中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数。bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都直接传给下层的函数(这种技术成为“部分应用”,是“柯里化”的一种)。例:

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

2.3.4 优先级判断

现在可以根据优先级来判断函数在某个调用位置应用的是哪条规则,判断顺序如下:

  1. 函数是否在 new 中调用(new绑定)? 若是的话, this 绑定的是新创建的对象。例: var bar = new foo()。
  2. 函数是否通过call、apply(显示绑定)或者硬绑定调用? 若是的话,this 绑定的是指定的对象。例:var bar = foo.call(obj1)。

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

  4. 若以上情况都不是的话,则使用默认绑定:若在严格模式下就绑定到 undefined,否则就绑定到全局对象。例:var bar = foo()。

对于正常的函数调用来说,以上就是this的绑定原理了,但规则总有例外。

2.4 绑定例外

2.4.1 被忽略的this

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

/**非严格模式下**/
function foo(){
    console.log(this.a);
}
var a = 2;
foo.call(null); //2


/**严格模式下**/
function foo(){
    "use strict";
    console.log(this.a);
}
var a = 2;
foo.call(null); //Uncaught TypeError: Cannot read property 'a' of null

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

function foo(a, b){
    console.log(`a: ${a}, b: ${b}`)
}

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

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

/**这两种方法都需要传入一个参数当作this的绑定对象,在函数不关心this的情况下,使用null是一个不错的选择**/

但总是使用null来忽略this可能会产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果(比如修改全局对象)。一种更安全的做法是传入一个特殊的对象,把this绑定到这个对象不会对程序产生任何副作用,它是一个空的非委托对象,在JavaScript创建一个空对象最简单的方法就是Object.create(null),可以用任何名字来命名这个对象。这种情况下任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。例:

function foo(a, b){
    console.log(`a: ${a}, b: ${b}`)
}
//创建空对象
var ø = Object.create(null); //可以任意命名表示空
//把数组“展开”成参数
foo.apply(ø, [2, 3]); //a: 2, b: 3

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

2.4.2 间接引用

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

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

赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是p.foo()或者o.foo(),按照2.2的绑定规则,这里会应用默认绑定规则。

2.4.3 软绑定

硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用时应用默认绑定规则。但硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this。而如果可以给默认绑定指定一个全局对象或者undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。可以通过一种被称为软绑定的方式实现这个期望:

if(!Function.prototype.softBind){
    Function.prototype.softBind = function(obj){
        //对指定的函数行径封装
        var fn = this;
        //捕获所有 curried 参数
        var curried = [].splice.call(arguments, 1);
        var bound = function(){
            //若this绑定到全局对象或者undefined,则把指定的默认对象obj绑定到this,否则不修改this
            return fn.apply((!this || this === (window || undefined)) ? obj : this, curried.concat.apply(curried, arguments));
        };
        bound.prototype = Object.create(fn.prototype);
        return bound;
    }
}

除了软绑定之外,softBind(..)的其他原理和ES5内置的bind(..)类似。这段代码还可支持可选的柯里化。

验证softBind(..)的软绑定功能:


function foo(){
    console.log(`name: ${this.name}`);
}
var obj1 = {name: "obj1"},
    obj2 = {name: "obj2"},
    obj3 = {name: "obj3"};
var fooOBJ = foo.softBind(obj1);

fooOBJ(); // name: obj1

obj2.foo = foo.softBind(obj1);
obj2.foo(); // name: obj2  <--  软绑定版本的 foo() 可以手动将this绑定到obj2上 - 隐式绑定

fooOBJ.call(obj3); // name: obj3  <--  软绑定版本的 foo() 可以手动将this绑定到obj3上 - 显示绑定

setTimeout(obj2.foo, 10); // name: obj1  <--  软绑定版本的默认绑定,将this绑定到了obj1上

2.5 this词法

2.2中介绍的四条绑定规则可以包含所有正常的函数,但ES6中出现的箭头函数并不能使用这些规则。箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。例:

function foo(){
    //返回一个箭头函数
    return (a) => {
        //this 继承自 foo()
        console.log(this.a);
    }
}
var obj1 = {
    a: 1
}

var obj2 = {
    a: 2
}

var bar = foo.call(obj1);
bar.call(obj2); // 1 

/**代码运行结果可看出,箭头函数指向的是foo()的this**/

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

箭头函数最常用于回调函数中,例如时间处理器或者定时器:

function foo(){
    setTimeout(() => {
        //这里的this在词法上继承自foo()
        console.log(this.a);
    }, 100);
}
var obj = {
    a: 0
}
foo.call(obj); // 0

箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。实际上,在ES6之前就已经在使用一种几乎和箭头函数完全一样的模式:

function foo(){
    var _this = this;
    setTimeout(() => {
        //这里的this在词法上继承自foo()
        console.log(_this.a);
    }, 100);
}
var obj = {
    a: 0
}
foo.call(obj); // 0

从本质上可以说,_this = this 和箭头函数都是想替代this机制。

关注:

  • this既不指向函数自身也不指向函数的词法作用域。
  • this是在运行时绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。
  • this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 
  • 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
  • 调用位置:是函数在代码中被调用的位置,而不是声明的位置。
  • 寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的 调用位置就在当前正在执行的函数的前一个调用中。
  • 必须先找到调用位置,然后判断需要应用哪条规则,最终确定this的绑定对象。

 

-- 《你不知道的JavaScript》上卷 第二部分 第1,2章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值