zoukankan      html  css  js  c++  java
  • 你不知道的JS(3)来聊聊this

    为什么要使用this?什么是this?

    来看一段代码

    function identify() {
        return this.name.toUpperCase();
    }
    function speak() {
        var greeting = "Hello, I'm " + identify.call( this );
        console.log( greeting );
    }
    var me = {
        name: "Kyle"
    };
    var you = {
        name: "Reader"
    };
    identify.call( me ); // KYLE
    identify.call( you ); // READER
    speak.call( me ); // Hello, 我是KYLE
    speak.call( you ); // Hello, 我是 READER

    如果不用this的话,我们就需要显式地传入一个上下文对象

    function identify(context) {
    return context.name.toUpperCase();
    }
    function speak(context) {
    var greeting = "Hello, I'm " + identify( context );
    console.log( greeting );
    }
    identify( you ); // READER
    speak( me ); //hello, 我是KYLE

    通过这个我们就可以了解到this的作用:隐式地传递上下文对象,避免代码耦合

    说完这个后,我们可以来描述下this:

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

    当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。

    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的绑定规则

    现在你知道了如何找到调用位置,这时候你还需要了解关于this绑定的四条规则


    1.默认绑定

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

    声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的.。

    接下来我们可以看到当调用foo() 时,this.a 被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this 的默认绑定,因此this 指向全局对象。

    在代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

    或者我们可以这么理解,foo()是被全局函数调用的,如window.foo()

    当函数的执行上下文环境是全局环境,那么就会使用默认绑定,即绑定到全局对象上

    不过,在严格模式下,就没有默认绑定了,this此时为undefined

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

    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 是一样的。也就是这里会查找foo时,会经过obj这个上下文对象,会把obj上下文对象保存下来,因此,这里的this指向的就是obj上下文对象。

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

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

    你可以这么理解:obj1=>obj2=>foo()。因此this找到了上下文对象后(obj2),就没必要继续去查找了,类似作用域链中查找变量。

    隐式丢失:

    一个最常见的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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

    或者可以这么理解,bar()函数指向的是一个匿名函数的引用,这时候已经和obj没有任何关系了,也就不存在obj上下文对象的引用了。

    var bar = function() {
        console.log( this.a );
    }; 

    一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:

    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"

    我们知道,参数传递其实是一种隐式赋值,也就是fn = obj.foo,所以结果和之前的例子一样。

    如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:

    function foo() {
        console.log( this.a );
    }
    var obj = {
        a: 2,
        foo: foo
    };
    var a = "oops, global"; // a 是全局对象的属性
    setTimeout( obj.foo, 100 ); // "oops, global"

    你可以这么理解:setTimeOut()把你的回调函数丢进去了任务队列中,然后JS引擎拿出来执行,这个执行环境的上下文其实就是全局上下文环境,因此也是使用默认绑定。

    就像我们看到的那样,回调函数丢失this 绑定是非常常见的。除此之外,还有一种情况this 的行为会出乎我们意料:调用回调函数的函数可能会修改this。

    在一些流行的JavaScript 库中事件处理器常会把回调函数的this 强制绑定到触发事件的DOM 元素上。这在一些情况下可能很有用,但是有时它可能会让你感到非常郁闷。遗憾的是,这些工具通常无法选择是否启用这个行为。

    无论是哪种情况,this 的改变都是意想不到的,实际上你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置。之后我们会介绍如何通过固定this 来修复/固定这个问题。


    3.显式绑定

    这个比较简单,就是使用call()和apply()函数。

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

    如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this 的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..) 或者new Number(..))。这通常被称为“装箱”。

    (1)硬绑定:

    显示绑定仍然可能存在着丢失this绑定的问题,因此我们需要采用硬绑定,也就是:创建要给函数,在函数内部再显示绑定,如这里的bar()

    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

    这个常用来创建包裹函数,用于包括所有接受到的值

    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

    或者说创建一个绑定的辅助函数,也就是bind

    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

    当然这个bind函数比起正式的bind()有很多不足,正是因为硬绑定很常用,所以才有了ES5的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 的上下文并调用原始函数。

    我们可以看下MDN是怎么实现的,当然这这只是一个polyfill版本的,因此还是会有.prototype,而ES5的bind()是没有.prototype的

    if (!Function.prototype.bind) {
      Function.prototype.bind = function(oThis) {
        if (typeof this !== 'function') {
          // closest thing possible to the ECMAScript 5
          // internal IsCallable function
          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
                     ? this
                     : oThis,
                     aArgs.concat(Array.prototype.slice.call(arguments)));
            };
    
        if (this.prototype) {
          // Function.prototype doesn't have a prototype property
          fNOP.prototype = this.prototype; 
        }
        fBound.prototype = new fNOP();
    
        return fBound;
      };
    }

    (2)API中的上下文

    很多函数比如迭代函数,都提供了一个参数用于传入函数上下文来绑定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();


    4.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内部机制也是使用了call或者apply函数,我们可以尝试实现New方法

    //实现一个new方法
    function New() {
        let obj = new Object(),
            Constructor = [].shift.call(arguments);
        obj.__proto__ = Constructor.prototype;
        let ret = Constructor.apply(obj, arguments);
        return typeof ret === 'object' ? ret : obj;
    
    };
    function foo(a) {
        this.a = a;
    }
    var bar = New(foo,2);
    console.log( bar); //foo { a: 2 }
    console.log( bar.a ); // 2

    绑定规则的优先级

    优先级:new绑定>显式绑定>隐式绑定>默认绑定

    注:ES6的箭头函数在四个规则以外,箭头函数的this值为词法作用域中的this值。

    判断this

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

    一些插曲:

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

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

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

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

    然而这种传入null的方式对于使用一些第三方库时可能产生副作用(把this绑到全局对象了),所以

    一种“更安全”的做法是传入一个特殊的对象,把this 绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个“DMZ”(demilitarized

    zone,非军事区)对象——它就是一个空的非委托的对象,比如我们可以ø = Object.create(null)创建一个空对象,以保护全局对象。

    //Object.create(null) 和{} 很像, 但是并不会创建Object.prototype 这个委托,所以它比{}“更空”

    function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
    }
    // 我们的DMZ 空对象
    var ø = Object.create( null );
    // 把数组展开成参数
    foo.apply( ø, [2, 3] ); // a:2, b:3
    // 使用bind(..) 进行柯里化
    var bar = foo.bind( ø, 2 );
    bar( 3 ); // a:2, b:3

    此外介绍下软绑定:用软绑定之后可以使用隐式绑定或者显式绑定来修改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;
        };
    }
  • 相关阅读:
    Android事件机制之一:事件传递和消费
    Android单个控件占父控件宽度一半且水平居中
    Android IllegalArgumentException: Cannot draw recycled bitmaps解决方法
    Android视图篇之一:Android常见基本布局
    Android Nine-patch(.9.png)小结
    adb server is out of date. killing... ADB server didn't ACK解决方法
    Docker 下自定义安装 Tomcat
    Docker 删除 images
    SecureCRT 取消右击粘贴功能
    如何将不同业务模块产生的日志 分多文件记录
  • 原文地址:https://www.cnblogs.com/wuguanglin/p/this.html
Copyright © 2011-2022 走看看