继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

一周一章前端书·第7周:《你不知道的JavaScript(上)》S02E02

SMILET
关注TA
已关注
手记 453
粉丝 80
获赞 441

第2章:this全面解析

2.1 调用位置

  • 在理解this的绑定之前,首先要理解“函数调用的位置”,即函数在代码中被调用的位置。只有仔细分析了调用位置才能解释,函数中的this到底引用的是什么?

  • 寻找“函数被调用的位置”,其实并没有想象中的简单,因为JS是很灵活的语言,经常将函数也作为参数进行传递,可能会隐藏真正的调用位置。

  • 所以我们需要分析函数的调用栈。所谓调用栈,就是为了到达当前执行位置,所调用过的所有函数,所以我们可以把调用栈想象成一个函数调用链,举例说明:

function baz(){    //当前位置是baz
    console.log("my name is baz");    //baz中调用bar
    bar();
}function bar(){    //当前位置是bar
    console.log("my name is bar");    //bar中调用foo
    foo();
}function foo(){    //当前位置是foo
    console.log("my name is foo");
}//window下调用bazbaz();/* 
 * 所以foo的调用栈(链)就是:
 * window -> baz -> bar -> foo
 * /

2.2 绑定规则

  • 通过函数的调用位置,并应用JavaScript中四条决定this绑定的规则,就能分析this的引用值了。

2.2.1 默认绑定

  • 首先是最常用的函数调用类型:独立函数调用。所谓独立函数调用,就是没有应用其他规则的默认调用规则。举例:

var a = 2;function foo(){    console.log(this.a);   
}
foo();  //输出 2
  • 当调用foo()函数时,this.a指向了全局变量a,因为在默认绑定下,this指向全局对象。

  • 那如何辨别这里应用的是默认绑定呢?这时候,就需要运用我们前面讲的“分析函数调用位置”了,在这段代码中,foo()函数是直接调用的,不带任何修饰,也不被任何函数包含,所以可以确定是默认绑定。

注意:如果 函数内使用严格模式(strict mode) ,是不能将全局对象用于默认绑定的,最终this会绑定到undefined上。举例说明:

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

在严格模式下调用函数 ,则不影响默认绑定。举例说明:

var a = 2;function foo(){  console.log(this.a);
}
(function(){  "use strict";
  foo():  // 输出2)();

由于我们可能会使用众多第三方库,所以代码中可能会混合使用strict模式和非strict模式,因此一定要注意这类的兼容性问题。

2.2.2 隐式绑定

  • 第二条规则,就是通过函数调用位置,函数是否属于某个对象的属性。

var obj = {    a : 2,    foo : foo
}function foo(){    console.log(this.a);
}

obj.foo();  // 输出 2
  • 你看foo()方法的声明方式,它是被当做引用属性添加到了obj对象中,这种情况下,obj对象拥有/包含了foo()方法。

  • 当函数有包含自己的对象时,隐式绑定规则会把this绑定到这个对象。

  • 因此,调用foo()时,this被绑定到了obj对象,在函数中this.aobj.a的引用是一样的。

  • 值得注意的是,如果是多层嵌套对象下的函数,就只在最后一层中起作用。举例:

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

obj1.obj2.foo();    //输出 42
  • 值得注意的是,如果将obj1.obj2.foo函数的引用赋值给另一个变量,然后以默认绑定的方式调用函数,不管是自定义的函数,还是JS的内置函数,则还是会应用默认绑定规则:

var a = 'oops,global';var bar = obj1.obj2.foo;function runFoo(){
    obj1.obj2.foo();
}// 都是输出 'oops,global'bar();  
runfoo();   
setTimeout(obj1.obj2.foo,100);

2.2.3 显示绑定

  • 如果不想在对象内部包含函数引用,想在某个对象上强制调用函数,该怎么做呢?

  • JavaScript中的函数都有一些特性,可以用来解决这个问题。比如函数的call()applay()方法

  • 这两个方法,传入的第一个参数是一个对象,就是留给this准备的,调用时会将其绑定到this。因为可以直接指定this的绑定对象,因此我们称之为显示绑定。

function foo(){    console.log(this.a);
}var obj = {    a : 2};
foo.call(obj);  // 输出 2
  • 但如果传入的参数是原始值(字符串、布尔或者数值类型)当做this的绑定对象的话,这个原始值会被转换成它的对象形式。也就是new String()new Boolean()或者new Number(),这个过程通常叫做装箱

  • 1. 硬绑定

function foo(){    console.log(this.a);
}var obj = {    a : 2};var bar = function(){
    foo.call(obj);
};

bar();  // 2setTimeout(bar,100);//硬绑定不能再修改它的thisbar.call(window);   // 2
  • 函数bar()在它内部手动调用了foo.call(obj),强制把foothis绑定到了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 3console.log(b); // 5
  • 另一种方式就是创建一个可以重复使用的辅助函数:

function foo(something){
    cosnole.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 3console.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 3console.log(b);    // 5
  • bind()会返回一个硬编码的新函数,它会把你指定的参数设置为this 的上下文,并调用原始函数。

  • 2. API调用的“上下文”

  • 许多函数都提供了一个可选的参数,其作用和bind()函数一样,确保你的回调函数使用指定的this。举例:

function foo(el){    console.log(el,this.id);
}var obj = {    id : 'awesome'}
[1,2,3].forEach(foo,obj);
  • 通过call()apply()实现显示绑定,可以少写代码。

2.2.4 new绑定

  • 在讲解最后一条this的绑定规则之前,首先要澄清一个常见的关于JavaScript中函数和对象的误解。

  • 在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类的构造函数,something = new MyClass()。然而,JavaScript中的new的机制实际上和面向类的语言完全不同。

  • 我们重新定义一些JavaScript中的“构造函数”:在JavaScript中,构造函数只是使用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绑定。

2.3 优先级

  • 上文通过大篇幅讲了函数调用中,this绑定的四条规则:默认绑定、隐式绑定、显示绑定和new绑定。但如果调用应用了多条规则就必须给这些规则设定优先级了。

  • 毫无疑问,默认绑定的优先级是最低的,暂不考虑它。

  • 隐式绑定和显示绑定哪一个优先级更高?我们来测试一下:

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

obj1.foo(); //2obj2.foo(); //3obj1.foo.call(obj2);    // 3obj2.foo.call(obj1);    // 3
  • 可以看到,显式绑定优先级更高。

  • 接下来,我们要测试,new绑定和隐式绑定的优先级谁高谁低:

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

obj1.foo(2);console.log(obj1.a);    //2obj1.foo.call(obj2,3);console.log(obj2.a);    //3var bar = new obj1.foo(4);console.log(obj1.a);    //2console.log(bar.a);    //2
  • 可以看到new绑定比隐式绑定的优先级更高,obj1.a的值一直没改变。

  • 那new绑定和显示绑定,谁的优先级更高呢?(由于new和call/apply无法一起使用,所以通过硬绑定来测试)

function foo(something){    this.a = something;
}var obj1 = {};var bar = foo.bind(obj1);
bar(2);console.log(obj1.a);    //2var baz = new bar(3);console.log(obj1.a);    // 2console.log(baz.a);     // 3
  • 观察输出的结果,bar被硬绑定到了obj1上,但new baz(3)并没有把obj1.a修改为3.

  • 话说回来,之所以在new中使用硬绑定函数,主要目的是想预先设置一些参数,这样在使用new进行初始化时就可以传入其他参数了。举例:

function foo(p1,p2){    this.val = p1 + p2;
}var bar = foo.bind(null,"p1");var baz = new bar("p2");
baz.val;    //p1p2
  • 根据优先级就能判断函数调用时应用的是哪条规则了,判断的步骤:

  1. 函数是否进行了new调用,如果是的话,this绑定的是新创建的对象;

  2. 函数是否通过callapply或者硬绑定调用,如果有的话,this绑定的是指定的对象;

  3. 函数是否在某个对象中调用,如果是的话,this绑定的是该对象;

  4. 如果都不是的话,使用默认绑定,绑定到全局对象。(严格模式下,绑定到undefined)

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
  • 什么场景下会传入null呢?比如使用apply来遍历输出一个数组,或者通过bind()进行柯里化:

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

foo.apply(null,[2,3]); // a:2,b:3//先预先传入参数avar bar = foo.bind(null,2);//调用时再传入参数bbar(3); // a:2,b:3
  • 但这种方式可能会导致许多难以分析和追踪的bug,我们可以用更安全的方式。

  • 更安全的做法就是不传入null,而是传入一个空的对象,把this绑定到这个对象,就好像创建一个非军事区的隔离对象一样,以确保不会对你的程序产生任何副作用。

function foo(a,b){    console.log('a:'+a+'b:'+b);
}var o = Object.create(null);
foo.apply(o,[2,3]); // a:2,b:3//先预先传入参数avar bar = foo.bind(o,2);//调用时再传入参数bbar(3); // a:2,b:3
  • 我们通过Object.create(null)来创建对象,它和直接以字面量{}创建对象很相似,但前者不会创建Object.prototype的委托,所以它比{}更空。

2.4.2 间接引用

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

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

o.foo(); // 3(function(){
    p.foo = o.foo
})();   // 2

2.4.3 软绑定

  • 硬绑定可以把this强制绑定到指定的对象,以防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,硬绑定之后,就无法使用隐式绑定或者显示绑定来修改this。

if(!Function.prototype.softbind){    Function.prototype.softbind = function(obj){        var fn = this;        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;
    }
}function foo(){    console.log('name:' + this.name);
}var obj = {        name : "obj"
    },
    obj2 = {        name : "obj2"
    },
    obj3 = {        name : "obj3"
    };var fooOBJ = foo.softbind(obj);

fooOBJ();   // name : objobj2.foo = foo.softbind(obj);
obj2.foo(); // name : obj2fooOBJ.call(obj3);  // name : obj3setTimeout(obj2.foo,10);    // name : obj

2.5 this词法

  • 前面解说的四条规则几乎也包含所有函数,但ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数

  • 箭头函数使用操作符=>来定义,箭头函数不适用this的四种标准规则,而是根据外层作用域来决定this。

function foo(){    return (a) => {        console.log(this.a);
    }
}var obj1 = {    a : 2}var obj2 = {    a : 3}var bar = foo.call(obj1);
bar.call(obj2); // 2 , 不是 3 !
  • 箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo(){
    setTimeout(() => {        console.log(this.a);
    },100);
}var obj = {    a : 2};
foo.call(obj); // 2

2.6 小结

  • 如果要判断一个运用中函数的this绑定,需要找到函数的调用位置,然后按顺序应用四条规则来判断this的绑定对象:

  1. 由new调用?绑定到新创建的对象;

  2. 由call或者apply/bind调用?绑定到指定的对象;

  3. 由对象调用?绑定到那个对象;

  4. 默认:严格模式下绑定到undefined,否则绑定到全局对象;

有些调用无意中使用默认绑定规则。如果想“更安全” 地忽略this绑定,可以使用一个空的临时对象,比如o = Object.create(null),以保护全局对象。

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



作者:梁同学de自言自语
链接:https://www.jianshu.com/p/0528aefd47d2


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP