zoukankan      html  css  js  c++  java
  • 《你不知道的JavaScript》 this

    1、为什么要用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 );
    identify.call( you );
    
    speak.call( me );
    speak.call( you);

      

    如果不是要this,需要给函数显式传入上下文对象。 

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

    this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简介并且易于复用。

    2、误解

    2.1 指向自身

    //记录函数foo被调用的次数
    function foo(num) {
        console.log( "foo: " + num);
        // 记录foo被调用次数
        this.count++;
    }
    
    foo.count = 0;
    
    var i;
    
    for (i = 0; i < 10; i++) {
        if(i > 5){
            foo( i );
        }
    }
    
    console.log( foo.count );
    
    
    //  foo: 6
    //  foo: 7
    //  foo: 8
    //  foo: 9
    
    //0

    console.log产生了4条输出,证明foo()被调用4次,但foo.count是0。this并非指向自身。

     

    function foo() {
         foo.count = 4;  //foo指向它自身
    }
    
    setTImeout(function() [
       //匿名函数无法指向自身
    }, 100)

    arguments.callee用于引用当前正在运行的函数对象。这是唯一可以从匿名函数对象内部引用自身的方法。但更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。

    强制this指向foo函数对象

    //记录函数foo被调用的次数
    function foo(num) {
        console.log( "foo: " + num);
        // 记录foo被调用次数
        this.count++;
    }
    
    foo.count = 0;
    
    var i;
    
    for (i = 0; i < 10; i++) {
        if(i > 5){
            //使用call( )可以确保this指向函数对象foo自身 
            foo.call(foo, i);
        }
    }
    
    console.log( foo.count );
    
    
    //  foo: 6
    //  foo: 7
    //  foo: 8
    //  foo: 9
    
    //4

    2.2 它的作用域

    第二章常见的误解是this指向函数的作用域,在某些情况下它是正确的,但其他情况下是错误的。

    this在任何情况下都不指向函数的词法作用域。在JavaScript内,作用域和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部。

    3、this是什么

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

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

    4、调用位置

    调用位置就是函数在代码被调用的位置。某些编程模式会隐藏真正的调用位置。最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数)。

    我们要关心的调用位置就在当前正在执行的函数的前一个调用中。

    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的调用位置

     

    5、绑定规则

    找到绑定位置,判断需要应用下面四条规则中的哪一条。

    5.1 默认绑定

    //独立函数调用
    //无法应用其他规则时的默认规则
    function foo( ) {
         console.log( this.a );
    }
    
    var a = 2;
    
    foo();  // 2

    foo( )直接使用不带任何修饰的函数引用进行调用,因此只能使用默认绑定,因此this指向全局对象。

    如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到undefined。

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

    虽然this的绑定完全取决于调用位置,但只有foo( )运行在非strict mode下,默认绑定才能绑定到全局对象,在严格模式下调用foo( )不影响默认绑定。

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

    5.2 隐式绑定

    另一条要考虑的规则是调用位置是否有上下文对象, 或者说是否被某个对象拥有或包含,不过这种说法可能会造成些误导。

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

    无论是直接在obj中定义还是先定义再添加为引用属性,foo严格来说都不属于obj对象。

    调用位置会使用obj上下文来引用函数,可以说函数被调用时obj对象“拥有”或“包含”它。当函数引用有上下文对象时,隐式绑定规则会把函数调用的this绑定到这个上下文对象。

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

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

    隐式丢失

    虽然bar是obj.foo的一个引用,但它引用的是foo函数本身,因此此时的bar( )是一个不带任何修饰的函数调用,应用了默认绑定。

    function foo() {
        console.log( this.a );
    }
    
    var obj = {
        a: 2,
        foo: foo
    }
    
    var bar = obj.foo;    // 函数别名
    
    var a = "oops, global";
    
    bar();    // "oops, global"

     参数传递是一种隐式赋值,因此传入函数时也会被隐式赋值。

    function foo() {
        console.log( this.a );
    }
    
    function doFoo(fn) {
        fn()    // <-- 调用位置
    }
    
    var obj = {
        a: 2,
        foo: foo
    }
    
    var a = "oops, global";
    
    doFoo( obj.foo );    // "oops, global"
    function foo() {
        console.log( this.a );
    }
    
    var obj = {
        a: 2,
        foo: foo
    }
    
    var a = "oops, global";
    
    setTimeout( obj.foo, 100);    // "oops, global"
    
    //JavaScript 环境中内置的setTimeout()函数实现和下面的伪代码类似:
    function setTimeout(fn, delay) {
        // 等待delay毫秒
        fn();    // <-- 调用位置
    }

    5.3 显式绑定

     call和apply方法第一个参数是对象,在调用时函数将其绑定到this,称之为显式绑定。

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

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

    5.3.1硬绑定 

    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

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

    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

    5.3.2API调用的“上下文”

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

    这些函数实际上通过call或apply实现了显式绑定。

    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

    5.4 new绑定

    在JavaScript,构造函数只是用new操作符时被调用的函数,不会属于某个类,也不会实例化一个类。 包含内置对象函数(如Number())在内的所有函数都可以用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上。

    6 优先级

    默认绑定的优先级是最低的。

    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

     显式绑定比隐式绑定优先级更高

    function foo(something) {
        this. a = something;
    }
    
    obj1 = {
        foo: foo;
    }
    
    obj2 = {};
    
    obj1.foo(2);
    console.log( obj1.a );  //  2
    
    obj1.foo.call( obj2, 3 );
    console.log( obj2.a );  //  3
    
    var bar = new obj1.foo(4);
    console.log( obj1.a);  //  2
    console.log( bar.a )  //  4

    new绑定比隐式绑定优先级高

    new和apply/call无法一起使用,因此无法通过new foo.call(obj1)来测试,但可以通过硬绑定测试。

    function foo(something) {
        this. a = something;
    }
    
    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

    new bar( 3 )没有把obj1.a修改为3,new修改了硬绑定(到obj1的)调用bar()中的this。因为使用了new绑定,得到一个名为baz的新对象,且baz.a值为3。

    在new中使用硬绑定主要是为了预先设置函数的一些参数,这样使用new进行初始化时就可以只传入其余的参数。bind()的功能之一是把除了第一个参数(第一个参数用于绑定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();

    7 绑定例外

    在某些情况下,你认为应用其他绑定规则时,实际上应用了默认绑定规则。

     7.1被忽略的this

     如果把nul或undefined作为this的绑定对象传入call、apply或bind,会应用默认绑定规则。

    一种常用的做法是使用apply展开一个数组并作为参数传入一个函数,bind可以对参数进行柯里化(预先设置一些参数):

    function foo(a, b) {
        console.log("a: " + a + ",b:" + b);
    }
    
    foo.apply( null, [2, 3]);
    
    var bar = foo.bind( null, 2);
    bar( 3 );

    总是使用null来忽略this会产生副作用。如果某个函数确实使用了this(比如第三方库的一个函数),那默认绑定规则会把this绑定到全局对象,可能修改全局对象。

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

    在JavaScript创建一个空对象最简单的方法是Object.create(null)。它不会像{}那样创建Object.prototype这个委托。

    function foo(a, b) {
        console.log("a: " + a + ",b:" + b);
    }
    var ∅ = Object.create(null);
    
    foo.apply(∅, [2, 3]);
    
    var bar = foo.bind( ∅, 2);
    bar( 3 );

    7.2间接绑定

    你可能有意或无意创建一个函数的”间接引用“ ,这时调用函数会应用默认绑定。

    间接引用最容易在赋值时发生:

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

    p.foo = o.foo返回值是目标函数的引用,因此调用位置是foo而不是p.foo或o.foo。

    对默认绑定,决定this绑定对象的不少调用位置是否处于严格模式,而是函数体是否处于严格模式。

    7.3软绑定

     硬绑定会降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或显式绑定来修改this。

    如果可以给默认绑定指定一个全局对象和undefined以外的值,就可以实现和硬绑定相同的效果,同时保留隐式绑定或显式绑定来修改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;
        };
    }
    
    function foo() {
        console.log("name " + this.name);
    }
    
    var obj = { name: "obj" },
        obj2 = { name: "obj2" },
        obj3 = { name: "obj3" };
    
    var fooOBJ = foo.softBind(obj);
    
    fooOBJ();  // name: obj
    
    obj2.foo = foo.softBind(obj);
    obj2.foo();    //    name: obj2
    
    fooOBJ.call(obj3);    //    name: obj3
    
    setTimeout(obj2.foo,10);    //name: obj  应用了软绑定 

    8 tihs词法

     ES6中的箭头函数不是要this的四种标准规则,而是根据外层(函数或全局)作用域来决定this。

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

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

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

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

    箭头函数可以像bind确保函数的this被绑定到指定的对象。

    在ES6之前已经在使用一种几乎和箭头函数完全一样的模式。

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

     

  • 相关阅读:
    设计者模式详解--代理模式
    设计者模式详解--适配器模式
    设计者模式详解--原型模式
    设计者模式详解--建造者模式
    设计者模式详解--单例模式
    设计者模式详解--抽象工厂模式
    设计者模式详解--工厂方法模式
    angularjs 选项卡 --- 自定义属性
    AngularJS 自定义指令
    Jquery中的prop()方法 全选或全不选
  • 原文地址:https://www.cnblogs.com/surahe/p/5953342.html
Copyright © 2011-2022 走看看