导航菜单
  • 首页
  • 首页>前端万博manbext备用网址>JavaScript万博manbext备用网址

    函数中的this对象的四种绑定规则和优先级!

    函数中的this是一个刚开始接触比较头疼的概念,总感觉它变来变去,害怕一不留神它就不是原来的它了。

    ES6用了箭头函数干脆把this固定住,不允许它变,但是在更复杂的场景中,我们恰恰需要this的多变性,所以,还是必须把this的绑定规则搞定。

    1、this对象

    this是JavaScript中一个很特别的关键字,被自动定义在所有函数的作用域中。在函数被调用的时候,this才具有指向性。this引用的是函数执行时的环境对象,也就是函数执行时的作用域对象。不是函数声明时的作用域对象。

    1.1、调用位置

    this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

    在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

    最重要的是要分析调用栈,就是为了到达当前执行位置所调用的所有函数,我们关心的调用位置就在当前正在执行的函数的前一个调用中。

    // 调用栈就是当前正在执行的函数
    // 调用位置就在当前正在执行的函数的前一个调用中。
    function baz(){
        //当前调用栈是:baz
        // 因此,当前调用位置是全局作用域
        console.log('baz');
        bar(); //bar的调用位置
        console.log('baz');
    }
    function bar(){
        // 当前调用栈是baz->bar
        // 因此,当前调用位置在baz中。
        console.log('bar');
        foo();//foo的调用位置
        console.log('bar');
    }
    function foo(){
        //调试命令,可以让程序停留在这里
        debugger;
        // 当前调用栈是baz->bar->foo
        // 因此,当前调用位置在bar中。
        console.log('foo');
    }
    
    baz(); //baz的调用位置

    1.2、绑定规则

    1.2.1、默认绑定

    最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则。

    function foo(){
        console.log(this.a);
    }
    //声明在全局作用域中的变量就是全局对象的一个属性。
    var a = 2;
    // 函数被调用时应用了this的默认绑定,this指向全局对象window
    foo(); // 2

    1.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绑定到这个上下文对象。

    对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

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

    1.2.3、隐式丢失

    一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined中。取决于是否是严格模式。(非严格模式绑定到全局对象,严格模式绑定到undefined中)

    function foo(){
        console.log(this.a);
    }
    var obj = {
        a:2,
        foo:foo,
    }
    //bar是obj.foo的一个引用,但是实际上,它引用的是函数foo本身。函数引用的上下文丢失
    var bar = obj.foo; // 函数别名!
    var a = '全局属性的值';
    //此时bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
    bar(); // 全局属性的值

    一种更常见的情况发生在传入回调函数时:

    function foo(){
        console.log(this.a);
    }
    //传入回调函数
    function doFoo(fn){
        //fn其实引用的是foo
        fn(); //foo的调用位置
    }
    var obj = {
        a:2,
        foo:foo
    }
    var a = '全局属性的值';
    //传递参数其实也是一种隐式赋值,传入函数时也会被隐式赋值。
    // fn = obj.foo 函数别名
    doFoo(obj.foo);//全局属性的值

    把函数传入语言内置的函数,情况也是一样的。

    function foo(){
        console.log(this.a);
    }
    var obj = {
        a : 2,
        foo : foo
    }
    var a = "全局属性的值";
    setTimeout(obj.foo,1000); //全局属性的值
    
    //和下面的代码原理相似:
    //fn = obj.foo 函数别名
    function setTimeout(fn,delay){
        //等待delay毫秒
        fn(); //调用位置
    
    }

    回调函数丢失this绑定是非常常见的。那么如何固定this呢?

    1.2.4、显式绑定

    隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。

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

    函数作为对象,也拥有自己的方法,call()和apply()方法,JavaScript提供的函数和自己创建的所有函数都可以使用call()和apply()方法。

    方法的第一个参数是一个对象,是给this准备的,在调用函数时将其绑定到this。因为可以直接指定this的绑定对象,这种方法称之为显式绑定。

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

    通过foo.call(),可以在调用foo()函数时强制把foo里面的this绑定到obj上。

    如果传入了一个原始值(字符串类型、布尔值类型、数字类型)来当作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(); // 2 
    setTimeout(bar, 100); // 2
    var a = '全局';
    //硬绑定的bar不可能再修改它的this
    bar.call(window); // 2

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

    function foo(something){
        console.log(this.a,something);
        return this.a + something;
    }
    var obj = {
        a:2
    }
    var bar = function(){
        //把bar的arguments参数传递给foo,apply支持传递arguments参数。
        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,它的用法如下:

    function foo(something){
        console.log(this.a,something);
        return this.a + something;
    }
    
    var obj = {
        a:2
    }
    //利用JavaScript提供的bind函数直接硬绑定
    var bar = foo.bind(obj);
    var b = bar(3); // 2 3
    console.log(b);//5
    console.log(bar === foo); // false

    bind()会返回一个新函数,它会把你指定的参数设置为this的上下文并调用原始函数。

    2、API调用的“上下文”

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

    function foo(el){
        console.log(el,this.id);
    }
    var obj = {
        id:'myid'
    }
    var id = '全局';
    //调用foo时把this绑定到obj
    [1,2,3].forEach(foo,obj);

    这些函数实际上就是通过call()等实现了显示绑定,可以少写一些代码实现this的绑定。

    1.2.5、new绑定

    这是最后一条this的绑定规则。

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

    1、创建一个全新的对象。

    2、这个新对象会被执行[[prototype]]连接。

    3、这个新对象会绑定到函数调用的this。

    4、如果函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象。

    function Foo(a){
        this.a = a ;
    }
    //使用new调用foo函数,会构造出一个新对象,并把这个新对象绑定到foo函数调用中的this上。
    var bar = new Foo(2);
    console.log(bar.a); // 2

    在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,它们并不是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。

    所以,所有函数都可以用new来调用,这种函数调用被称为构造函数调用,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

    1.3、优先级

    默认绑定的优先级是四条规则中最低的。

    显示绑定的优先级高于隐式绑定。

    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

    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(obj2.a);//3
    //new绑定比隐式绑定的优先级高
    var bar = new obj1.foo(4);
    console.log(bar.a); // 4
    
    console.log(obj1.a);//2

    new绑定的优先级高于显示绑定

    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

    现在可以根据优先级来判断函数在某个调用位置应用的是哪条规则。

    可以按照下面的顺序进行判断:

    1、函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

    var bar = new Foo()

    2、函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

    var bar = foo.call(obj2)

    3、函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    var bar = obj.foo()

    4、如果都不是的话,使用默认绑定。如果是在严格模式下,就绑定到undefined,否则绑定到全局对象。

    var bar = foo()

    1.4、绑定例外

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

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

    什么情况下会传入null呢?

    一种常用的做法是用apply()来展开一个数组,并当做参数传入一个函数。

    function foo(a,b){
        console.log(a + b);
    }
    // 把数组展开成参数
    foo.apply(null,[2,3]); // 5
    //使用bind()进行柯里化
    var bar = foo.bind(null,2);
    bar(3); // 5

    这两种方法都需要传入一个参数当做this的绑定对象,如果函数不关心this的话,仍然需要传入一个占位值,null非常方便。

    let arr = [12,45,14,38];
    let max = Math.max.apply(null,arr);
    console.log(max); // 45

    然而,总是使用null来忽略this绑定可能产生一些副作用,如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定会把this绑定到全局对象window上,这可能会导致不可预计的后果(比如修改全局对象)。所以,使用null会导致难以分析和追踪的bug。

    更安全的this

    一种更安全的方法是传入一个特殊的对象,把this绑定到这个对象不会对程序产生任何副作用。

    通过创建一个空对象,就不会对全局对象产生任何影响。

    // 创建一个空对象,这个空对象比{}更空,因为并不会创建Object.prototype对象。
    var φ = Object.create(null);
    // console.log(φ,{})
    function foo(a,b){
        console.log(a + b);
    }
    // 把数组展开成参数
    foo.apply(φ,[2,3]); // 5
    //使用bind()进行柯里化
    var bar = foo.bind(φ,2);
    bar(3); // 5

    2、this词法

    在ES6中有一种无法使用上面四种规则的特殊函数类型:箭头函数。

    箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

    function foo(){
        //返回一个箭头函数
        return a => {
            //this继承自foo()
            console.log(this.a);
        };
        /* return function(a){
               console.log(this.a);
           }; */
    }
    let obj1 = {
        a:2
    };
    let obj2 = {
        a:3
    };
    
    let bar = foo.call(obj1);
    bar.call(obj2);// 2

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

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

    function foo(){
        setTimeout(()=>{
            //这里的this在词法上继承自foo()
            console.log(this.a);
        },1000)
    }
    
    let obj1 = {
        a:2
    };
    foo.call(obj1); // 2

    箭头函数可以像bind()一样确保函数的this被绑定到指定对象上,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。

    在ES6之前,有一种几乎和箭头函数完全一样的模式:

    function foo(){
        //把this对象保存在变量中
        var _this = this;
        setTimeout(function(){
            //通过_this变量来使用保存好的this对象
            console.log(_this.a);
        },1000)
    }
    
    let obj1 = {
        a:2
    };
    foo.call(obj1); // 2

    3、构造函数

    其实,在JavaScript中,构造函数并不是一种特殊的函数,只是受到Java等面向对象,拥有类概念的语言的影响。

    构造函数首字母要大写这种规矩也是来源于对Java等语言的追随。

    function Foo(){
        return this;
    }
    let o = new Foo();
    
    console.dir(Foo);//prototype是函数的一个属性,可以叫做原型对象。
    //函数的prototype对象里面有一个constructor属性,又指向函数本身。
    console.log(Foo.prototype.constructor === Foo); // true
    //o是一个实例,它有一个原型链[[Prototype]]属性,指向new出自己的函数的原型对象。
    console.log(o);
    //通过__proto__可以访问实例的原型对象。
    console.log(o.__proto__);
    //o实例并没有constructor属性,但是通过对原型链的往上层访问,可以访问到原型对象里面的constructor属性。
    console.log(o.constructor === Foo);// true

    实际上,Foo函数与其他函数没有任何区别。函数本身不是构造函数,当在普通函数调用前面加上new关键字后,就会把这个函数调用变成一个“构造函数调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。

    function nothingSpecial(){
        console.log('我是一个普通函数');
    }
    
    let obj = new nothingSpecial();
    console.log(obj); // {}

    nothingSpecial()只是一个普通函数,使用new调用的时候,就会产生一个对象并赋值给obj,使用new调用一个函数无论如何都会返回一个对象。这个new的调用是一个构造函数的调用,但是nothingSpecial()却不是一个 构造函数。

    可以理解为在JavaScript中,对于“构造函数”最准确的解释是:所有带new的函数调用。

    函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。

    当函数作为构造函数调用的时候,函数内部的this指向new返回出来的对象。

    点赞


    1
    保存到:

    相关文章

    发表评论:

    ◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。

    Top