美文网首页JavaScriptWeb前端之路程序员
你不知道的JavaScript之this篇(二)

你不知道的JavaScript之this篇(二)

作者: 2f1b6dfcc208 | 来源:发表于2017-07-31 10:28 被阅读12次

    上篇记录了对于this的一些误解,并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置。

    调用位置
    在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用位置才能明白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的调用位置
    

    绑定规则
    接下来我们看看在函数的执行过程中调用位置如何决定this的绑定对象

    1、 默认绑定
    首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则

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

    这里的this指向全局对象,首先,声明在全局作用域中的变量就是全局对象的一个同名属性。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。注意:如果使用严格模式,则不能将全局对象应用于默认绑定,此时this会绑定到undefined。

    2、隐式绑定
    另一条需要考虑的规则是调用位置是否有上下文对象

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

    当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。举例来说:

    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"
    bar();//"oops,global"
    

    此处bar是obj.foo的一个引用,但实际上它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
    一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时,

    function foo(){
        console.log(this.a);
    }
    function dofoo(fn){
        fn();
    }
    var obj={
        a:2,
        foo:foo
    }
    var a="oops,global";
    doFoo(obj.foo)
    

    书上说:参数传递其实就是一种隐式赋值,因此我们传入函数时同样会被隐式赋值,所以结果和上面的例子一样,在函数里面调用时同样引用的是foo函数本身,因此应用了默认绑定。
    就像我们看到的那样,回调函数丢失this绑定是非常常见的。除此之外,还有一种情况this的行为会出乎我们的意料,调用回调函数的函数可能会修改this,在以前一些流行的js库中事件处理器会把回调函数中的this强制绑定到触发事件的DOM元素上,这在一些情况下可能很有用,但是有时可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。无论是哪种情况,this的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制调用位置以得到期望的绑定。后面我们会介绍如何通过固定this来修复这个问题。

    3、显式绑定
    如同上面的笔记,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把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(...),这通常被称作“装箱”)。call和apply的区别在于apply的后续参数是参数数组,call的后续参数是参数列表。
    然而,显式绑定仍然无法解决我们之前提出的丢失绑定问题。
    (1)硬绑定
    硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:

    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 obj1={
        a:2
    }
    var bar=bind(foo,obj1)
    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的上下文。
    (2)API调用的“上下文”
    第三方库中的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(...)一样,确保将回调函数中的this指向传入的上下文参数

    function foo(el){
        console.log(e,this.id);
    }
    var obj={
        id:"awesome"
    }
    [1,2,3].forEach(foo,obj);//这里调用foo时把this绑定到obj
    

    这些函数实际上就是通过call(...)或者apply(...)实现了显示绑定,这样可以少写一些代码。

    4、new绑定
    在讲解最后一条this绑定规则之前,首先需要了解一个非常常见的关于JavaScript中函数和对象的误解。在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数,进行实例化,通常的形式是var something=new MyClass(...).但是js中实际上没有类,也就没有实例化这个概念,js中一切都是对象,但是js中也有一个new操作符,使用方法看起来和那些面向类的语言一样,然而,js中new的机制实际上和面向类的语言完全不同。首先我们重新定义一下js中的"构造函数",在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。举例来说,当Number(...)作为构造函数被调用时,ES5.1中这样描述:

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

    所以,包括内置对象函数(比如Number(...)在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
    在使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
    (1)创建或者说构造一个全新的对象
    (2)这个新对象会被执行[[Prototype]]连接
    (3)这个新对象会绑定到函数调用的this
    (4)如果函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象
    使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。

    相关文章

      网友评论

        本文标题:你不知道的JavaScript之this篇(二)

        本文链接:https://www.haomeiwen.com/subject/tvtllxtx.html