• Javascript中的 this


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

    this不是编写时绑定的,而是运行时绑定。它依赖于函数调用的上下文条件。this绑定与函数声明的位置没有任何关系,而与函数被调用的方式紧密相连。

    当一个函数被调用时,会建立一个称为执行环节的活动记录。这个记录包含函数是从何处(调用栈---call-stack)被调用的,函数是如何被调用的,被传递了什么参数等信息。这个记录的属性之一,就是在函数执行期间将被使用的this引用。

    this 机制提供了更优雅的方式来隐含地“传递”一个对象引用,导致更加干净的API设计和更容易的复用。

    你的使用模式越复杂,你就会越清晰地看到:将执行环境作为一个明确参数传递,通常比传递 this 执行环境要乱.

    对this的困惑

    想当初,对"this"这个名字用太过于字面的方式考虑而产生了困惑,没有真正的了解this是如何实际工作的。接下来首先要摒弃一些误解。

    一:this 指向函数自己

    看一个例子:

    function fn(num) {
    	console.log( "fn: " + num );
    
    	// 追踪 `fn` 被调用了多少次
    	this.count++;
    }
    fn.count = 0;
    for (let i=0; i<5; i++) {
    	fn( i );
    }
    // fn: 0
    // fn: 1
    // fn: 2
    // fn: 3
    // fn: 4
    
    console.log(fn.count); // 0
    

    复制代码到控制台打印一下,what??? fn.count为什么是0,console.log( "fn: " + num )明明告诉我们实际调用五次的,初学时的就是这种状态 --- N脸懵逼。这种错误的认为就是我们对于this(在 this.count++ 中)的含义进行了过于字面化的解释。

    为什么会出现这种情况??
    记住,this的指向与所在方法的调用位置有关,而与方法的声明位置无关。 this.count++这个语句,不小心创建了一个全局变量,此时的this是指向的全局对象。在非严格模式下,全局作用域中函数被独立调用时,它的this默认指向(绑定)window或者global。在严格模式中,它的this为undefined。

    这个问题的解决方法有多种,这里给出其中一种-强迫thi指向fn函数对象

    function fn(num) {
    	console.log( "fn: " + num );
    
    	// 追踪 `fn` 被调用了多少次
    	this.count++;
    }
    fn.count = 0;
    for (let i=0; i<5; i++) {
    	fn.call(fn, i );
    }
    // fn: 0
    // fn: 1
    // fn: 2
    // fn: 3
    // fn: 4
    
    console.log(fn.count); // 5
    

    二:this 函数的作用域

    this不会以任何方式指向函数的词法作用域。作用域好像是一个将所有可用标识符作为属性的对象,这从内部来说是对的。但是 JavasScript 代码不能访问作用域“对象”。它是 引擎 的内部实现。

    看一个例子:

    function fn() {
    	var a = 2;
    	this.bar(); // 这里的this指向全局对象window
    }
    
    function bar() {
    	console.log( this.a ); // 这里的this指向全局对象window
    }
    
    fn(); // undefined
    
    

    没想到吧,最后输出咋不是2呢?瞎搞了吧哈哈
    如果你想用 this 在 fn() 和 bar() 的词法作用域间建立一座桥,使得bar() 可以访问 fn()内部作用域的变量 a。这是不可能的,你不能使用 this 引用在词法作用域中查找东西。如果你想要函数bar访问函数fn里的a 变量,可以用闭包实现:

    function fn() {
    	var a = 2;
      (function b() {
        console.log( a );
      })()
    }
    
    fn(); // 2
    

    调用点

    先来了解一下调用点,调用点决定函数执行期间 this 指向哪里。

    用代码来展示一下调用栈和调用点:

    function baz() {
        // 调用栈是: `baz`
        // 我们的调用点是 global scope(全局作用域)
    
        console.log( "baz" );
        bar(); // <-- `bar` 的调用点
    }
    
    function bar() {
        // 调用栈是: `baz` -> `bar`
        // 我们的调用点位于 `baz`
    
        console.log( "bar" );
        foo(); // <-- `foo` 的 call-site
    }
    
    function foo() {
        // 调用栈是: `baz` -> `bar` -> `foo`
        // 我们的调用点位于 `bar`
    
        console.log( "foo" );
    }
    
    baz(); // <-- `baz` 的调用点
    

    绑定规则

    先来了解一下4种绑定规则,用于考察调用点并判定4种规则中的哪一种适用。

    默认绑定

    1. 默认绑定常见的情况是独立函数调用,也就是 fn()。这时 this 指向了全局对象 window

    看一个例子:

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

    再看例子,给上文误解的例子加深理解:

    var count = 0;
    function fn(num) {
    	console.log( "fn: " + num );
    
    	// 追踪 `fn` 被调用了多少次
    	this.count++;
    }
    fn.count = 0;
    for (let i=0; i<5; i++) {
    	fn( i );
    }
    // fn: 0
    // fn: 1
    // fn: 2
    // fn: 3
    // fn: 4
    
    console.log(fn.count); // 0
    console.log(count); // 5
    
    1. 严格模式 下,对于 默认绑定 来说全局对象是不合法的,this 将被设置为 undefined

    看一个例子:

    function fn() {
    	"use strict";
    
    	console.log( this.a );
    }
    
    var a = 2;
    
    fn(); // TypeError: `this` is `undefined`
    
    1. 显示使用 window 调用 fn() 则不会报错。

    看一个例子:

    function fn() {
    	"use strict";
    
    	console.log( this.a );
    }
    
    var a = 2;
    
    window.fn(); // 2
    
    1. 即便所有的 this 绑定规则都是完全基于调用点的,但如果 fn() 的内容没有在 严格模式 下执行,对于默认绑定来说全局对象是唯一合法的。fn() 调用点的 严格模式 状态与此无关。

    看一个例子:

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

    注意: 应尽量避免 严格模式非严格模式混淆使用。

    隐式绑定

    这个规则是: 调用点是否有一个环境对象。当一个方法引用存在一个环境对象时, 这个对象应当被用于这个函数调用的 this 绑定。

    1. 函数如果被某个对象包含,且作为这个对象的方法调用时,函数里面的 this 就是这个对象。

    看一个例子:

    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      foo: foo
    }
    obj.fn(); // 2
    
    1. 只有对象属性引用链的最后一层是影响调用点的。

    看一个例子:

    function fn() {
      console.log(this.a);
    }
    var obj1 = {
      a: 42,
      foo: foo
    }
    var obj2 = {
      a: 2,
      obj1: obj1
    }
    obj2.obj1.foo(); // 42
    
    1. 当一个隐式绑定丢失了它的绑定,意味着它可能会退回到默认绑定。

    看一个例子:

    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      fn: fn
    };
    var bar = obj.fn; // 函数引用!
    var a = 'oops, global'; // `a` 也是一个全局对象的属性
    bar();  // "oops, global"
    
    
    1. 当传递一个回调函数式,参数传递仅仅是一种隐式的赋值,而且因为是传递一个函数,它是一个隐含的引用赋值,所以最终结果也是指向了全局对象,触发了默认绑定,this 绑定到 window中了。

    看一个例子:

    function fn() {
    	console.log( this.a );
    }
    
    function dofn(fn) {
    	// `fn` 只不过 `fn` 的另一个引用
    
    	fn(); // <-- 调用点!
    }
    
    var obj = {
    	a: 2,
    	fn: fn
    };
    
    var a = "oops, global"; // `a` 也是一个全局对象的属性
    
    dofn( obj.fn ); // "oops, global"
    
    

    或者

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

    显式绑定

    利用 JavaScript 提供的的 call, apply, bind方法强制一个函数使用某个特定的对象作为this的绑定。

    1. 通过 call或者 apply 强制函数的 this 指向 fn .

    看一个例子:

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

    注意: 如果你传递一个简单基本类型值(string,boolean,或 number 类型)作为 this 绑定,那么这个基本类型值会被包装在它的对象类型中(分别是 new String(..),new Boolean(..),或 new Number(..))。这通常称为“封箱(boxing)”。

    说明: 就 this 绑定的角度讲,call(..) 和 apply(..) 是完全一样的。它们在处理其他参数上的方式不同,call的参数列表要一个一个列在后面,如fn.call(obj, '参数一', '参数二'), 而apply参数列表使用的是数组,如fn.apply(obj, ['参数一', '参数二'])

    1. 硬绑定 bind
      ECMAScript 5 引入了 Function.prototype.bind。 bind(..)会创建一个与原本函数具有相同函数体和作用域的函数,但是在这个心函数中,this 将永久被绑定到了 bind的第一个参数,无论这个函数如何被调用的。

    看一个例子:

    function fn(something) {
    	console.log( this.a, something );
    	return this.a + something;
    }
    
    var obj = {
    	a: 2
    };
    
    var bar = fn.bind( obj );
    
    var b = bar( 3 ); // 2 3
    console.log( b ); // 5
    

    注意: 在 ES6 中,bind(..) 生成的硬绑定函数有一个名为 .name 的属性,它源自于原始的 目标函数(target function)。举例来说:bar = fn.bind(..) 应该会有一个 bar.name 属性,它的值为 "bound foo",这个值应当会显示在调用栈轨迹的函数调用名称中。

    new 绑定

    当函数使用 new 调用时,即构造器调用时,会自动执行下面的操作:

    1. 一个全新的对象会凭空创建(就是被构建)
    2. 这个新构建的对象会被接入原形链([[Prototype]]-linked)
    3. 这个新构建的对象被设置为函数调用的 this 绑定
    4. 除非函数返回一个它自己的其他对象,否则这个被 new 调用的函数将自动返回这个新构建的对象。

    看个例子:

    function fn(a) {
    	this.a = a;
    }
    
    var bar = new fn( 2 );
    console.log( bar.a ); // 2
    
    

    new 的创建过程:

    var new2 = function(func) {
      var o = Object.create(func.prototype);
      var k = func.call(o);
      if(typeof k === 'object') {
        return k;
      } else {
        return o;
      }
    }
    
    

    四种绑定规则的顺序

    1. 函数是通过 new 被调用的吗(new 绑定)?如果是,this 就是新构建的对象。
    var bar = new foo()
    
    1. 函数是通过 callapply 被调用(明确绑定),甚至是隐藏在 bind 硬绑定 之中吗?如果是,this 就是那个被明确指定的对象。
    var bar = foo.call( obj2 )
    
    1. 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,this 就是那个环境对象。
    var bar = obj1.foo()
    
    1. 否则,使用默认的 this(默认绑定)。如果在 strict mode 下,就是 undefined,否则是 global 对象。
    var bar = foo()
    

    箭头函数里的 this

    箭头函数不是通过 function 关键字声明的,而是通过所谓的“大箭头”操作符:=>。
    箭头函数是从封闭它的(函数或全局)作用域采用 this 绑定。

    看一个例子:

    function fn() {
      // 返回一个箭头函数
    	return (a) => {
        // 这里的 `this` 是词法上从 `fn()` 采用的
    		console.log( this.a );
    	};
    }
    
    var obj1 = {
    	a: 2
    };
    
    var obj2 = {
    	a: 3
    };
    
    var bar = fn.call( obj1 );
    bar.call( obj2 ); // 2, 不是3!
    

    最常见的用法是用于回调,比如setTimeout

    function fn() {
    	setTimeout(() => {
    		// 这里的 `this` 是词法上从 `fn()` 采用
    		console.log( this.a );
    	},100);
    }
    
    var obj = {
    	a: 2
    };
    
    fn.call( obj ); // 2
    

    总结

    • this 总是指向调用它所在方法的对象
    • this 的指向与所在方法的调用位置有关,而与方法的声明位置无关
    • 在浏览器中,调用方法时没有明确对象的, this 指向 windowNode 中,这种情况指向 global,但是在 Node cli 下,与浏览器的行为保持一致。严格模式下,thisundefined
    • 在浏览器中 setTimeoutsetInterval 和匿名函数执行时的当前对象是全局对象 window
    • applycall 能够强制改变函数执行时的当前对象,让 this 指向其他对象
    • es6开始,出现箭头函数(lamda表达式), 是在声明时候绑定this的
    • [绑定特例],自行查看书籍(https://github.com/CuiFi/You-Dont-Know-JS-CN/blob/master/this %26 object prototypes/ch2.md#绑定的特例)

    学习链接
    你不懂JS: this 是什么?
    你不懂JS: this 豁然开朗!
    this - JavaScript | MDN 官方中文版
    JavaScript 的 this 原理

  • 相关阅读:
    [网络流24题] 最长k可重区间集问题 (费用流)
    [网络流24题] 方格取数问题/骑士共存问题 (最大流->最大权闭合图)
    [网络流24题] 太空飞行计划问题 (最大流->最大权闭合图)
    [网络流24题] 最小路径覆盖问题 (最大流/匈牙利 二分图匹配)
    [网络流24题] 试题库问题 (最大流)
    [网络流24题] 运输问题 (费用流)
    luogu P4364 [九省联考2018]IIIDX
    loj 6031「雅礼集训 2017 Day1」字符串
    CF702F T-Shirts
    uoj #46[清华集训2014]玄学
  • 原文地址:https://www.cnblogs.com/arissy/p/10173002.html
Copyright © 2020-2023  润新知