zoukankan      html  css  js  c++  java
  • 《你不知道的JavaScript》第二部分:this 详解

    第1章 关于this

    this 是自动定义在所有函数的作用域中的关键字,用于引用合适的上下文对象

    ☞ 为什么要使用 this
    • this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
    ☞ 对 this 的误解
    • this 不指向函数自身,也不指向函数的词法作用域。

      • 作用域“对象”无法通过JavaScript代码访问,存在于JavaScript引擎内部
    this 到底是什么
    • this 是在函数被调用时发生的绑定,和函数声明的位置没有关系,它的上下文(指向)取决于函数调用时的各种条件。
    • 当一个函数被调用时,会创建一个活动记录(执行上下文)。
      • 这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

    第2章 this全面解析

    ☞ 调用位置(调用方法)

    调用栈:为了到达当前执行位置所调用的所有函数,类似于函数调用链。

    调用位置就是在当前执行的函数的前一个调用中。

    function baz() {
      // 当前调用栈是:baz
      // 因此,当前调用位置是全局作用域
    
      console.log('baz');
      bar();  // <-- bar 的调用位置
    }
    
    function bar() {
      // 当前调用栈是:baz -> bar
      // 因此,当前调用位置在 baz 中
    
      console.log('bar');
    }
    
    baz();  // <-- baz 的调用位置
    

    ☞ 绑定规则

    ① 默认绑定:独立函数调用,this 指向全局对象
    	function foo() {
    	  console.log(this.a);
    	}
    	
    	var a = 2;
    	foo();  // 2
    

    严格模式下,全局对象将无法使用默认绑定,this 会绑定到 undefined

    ② 隐式绑定:考虑调用位置是否有上下文对象
    	function foo() {
    	  console.log(this.a);
    	}
    	
    	/**
    	 * 无论是直接在 obj 中定义还是先定义再添加引用属性,foo() 严格来说都不属于 obj 对象
    	 */
    	var obj2 = {
    	  a: 42,
    	  foo: foo	// 当做 obj 的引用属性添加
    	};
    	
    	var obj1 = {
    	  a: 2,
    	  obj2: obj2
    	}
    	
    	/**
    	 * 对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
    	 * 调用位置使用 obj2 的上下文来引用函数
    	 */
    	obj1.obj2.foo();  // 42
    

    当函数有上下文对象时,隐式绑定 规则会把函数调用中的 this 绑定到这个上下文对象。

    ※ 隐式丢失:被 隐式绑定 的函数会丢失绑定对象,即会应用 默认绑定

    [例1:]

    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2,
      foo: foo
    };
    
    var bar = obj.foo;  // 函数别名!
    var a = "oops, global"; // a是全局对象的属性
    bar();  // 'oops, global'
    

    [例2:传入回调函数 ]

    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'
    
    // 传入语言内置的函数
    setTimeout(obj.foo, 2000); // 'oops, global'
    

    JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:

    function setTimeout(fn, delay) {
      // 等待 delay 毫秒
      fn(); // <-- 调用位置
    }
    

    调用回调函数的函数可能会修改 this。

    在一些流行的 JavaScript 库中事件处理器常会把回调函数的 this 强制绑定到触发事件的 DOM 元素上。

    实际上,你无法控制回调函数的执行方式,因此就没有办法控制会影响绑定的调用位置。

    ③ 显式绑定:call()、apply()

    1) 硬绑定 :显式绑定的一个变种,解决丢失绑定问题

    function foo() {
      console.log(this.a);
    }
    
    var obj = {
      a: 2
    };
    
    var a = 3;
    
    /**
     * 显式绑定
     * 仍然存在丢失绑定问题
     */
    foo.call(obj);  // 2
    foo.call(null); // 3
    
    /**
     * 硬绑定:显式的强制绑定
     * 解决丢失绑定问题
     */
    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
    

    [ 硬绑定的应用场景2 ]:创建一个 i 可重复使用的辅助函数(bind实现及内置函数)

    function foo(something) {
      console.log(this.a, something);
      return this.a + something;
    }
    
    /**
     * 简单的辅助绑定函数:
     * 返回一个硬编码的新函数,把参数设置为 this 的上下文并调用原始函数
     */
    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
    
    /**
     * 硬绑定模式内置方法:
     * Function.prototype.bind
     */
    var bar2 = foo.bind(obj);
    
    var b2 = bar(4);  // 2 4
    console.log(b2); // 6
    

    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"
    
    new 绑定

    在 JavaScript 中,构造函数只是有些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

    使用 new 来调用函数时,会自动执行下面的操作:

    1)创建(或者说构造)一个全新的对象;

    2)这个新对象会被执行[[原型]]连接;

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

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

    ☞ 优先级/判断 this

    1. new 调用?——> 绑定到新创建的对象。
    2. callapply(或者 bind)调用?——> 绑定到指定的对象。
    3. 由上下文对象调用?——> 绑定到那个上下文对象。
    4. 默认——> 在严格模式下绑定到 undefined,否则绑定到全局对象。

    [例子]:

    function foo(p1, p2) {
      this.val = p1 + p2;
    }
    
    // 是所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
    // 反正使用 new 时 this 会被修改
    var bar = foo.bind(null, 'p1');
    
    var baz = new bar('p2');
    
    console.log(baz.val); // p1p2
    

    new 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用 new 进行初始化时就可以只传入其余的参数。

    bind(...)的功能之一就是可以把第一个参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术成为“部分应用”,是“柯里化”的一种)。

    ☞ 绑定例外

    ① 被忽略的 this

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

    • 传入 null 的情况:

      • 使用 apply(...) 来“展开”一个数组,并当做参数传入一个函数。

      • bind(...)可以对参数进行柯里化(预先设置一些参数)。

          function foo(a, b) {
            console.log("a:" + a + ", b:" + b);
          }
          
          // 把数组“展开”成参数
          foo.apply(null, [2, 3]); // a:2, b:3
          
          // 在ES6中,可以用...操作符代替apply(...)来“展开”数组
          foo(...[1,2]);  // a:1, b:2
          
          // 使用 bind(...) 进行柯里化
          var bar = foo.bind(null, 3);
          bar(4); // a:3, b:4
        
    • 更安全的 this

      • 创建一个空的非委托的对象(Object.create(null)

          function foo(a, b) {
            console.log("a:" + a + ", b:" + b);
          }
          
          // 创建DMZ(demilitarized zone,非军事区)空对象
          var dmzObj = Object.create(null);
          
          // 把数组“展开”成参数
          foo.apply(dmzObj, [2, 3]); // a:2, b:3
          
          // 使用 bind(...) 进行柯里化
          var bar = foo.bind(dmzObj, 3);
          bar(4); // a:3, b:4
        

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

    ② 间接引用 —— 函数会应用默认绑定规则。

    [ “间接引用”最容易在赋值时发生 ]:

    function foo() {
        console.log(this.a);
    }
    
    var a = 2;
    var o = {a: 3, foo: foo};
    var p = {a: 4};
    
    o.foo();  // 3
    
    /**
     * 该赋值表达式的返回值是目标函数的引用
     * 因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()
     */
    (p.foo = o.foo)();  // 2
    
    ③ 软绑定

    硬绑定的优点:会把 this 强制绑定到指定的对象,防止函数调用应用默认绑定规则。

    硬绑定的缺点:会大大降低函数的灵活性,使用之后就无法使用隐式绑定或者显示绑定来修改 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    <---- 应用了软绑定
    
    /**
     * 硬绑定
     */
    obj3.foo = foo.bind(obj3);
    obj3.foo(); // name: obj3 
    setTimeout(obj3.foo, 10);   // name: obj3
    

    this 词法 ——> 箭头函数

    箭头函数 不使用 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!箭头函数的绑定无法被修改!
    

    [ 箭头函数最常用于回调函数中 ]:

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

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

    Scoop It and Enjoy the Ride!
  • 相关阅读:
    VS2012 打包部署程序
    请求筛选模块被配置为拒绝包含 hiddenSegment 节的 URL 中的路径
    “远程服务器返回错误: (404) 未找到”的正确解决方法
    23.IDEA 运行junit单元测试方法
    Java单元测试之JUnit篇
    22.IntelliJ IDEA 切换 project
    21. 【intellij idea】Project Structure 讲解
    一个多maven项目聚合的实例
    解决Maven项目相互依赖/循环依赖/双向依赖的问题
    20. idea刷新项目、清除项目缓存
  • 原文地址:https://www.cnblogs.com/Ruth92/p/5681636.html
Copyright © 2011-2022 走看看