Loading

JS 详谈This指向

从原理出发

什么是this? this是被调用函数对调用它上下文对象的引用。
首先我们围绕耳熟能详的“this始终指向它的调用者”开始。这句结论虽然没有什么问题,但是说得过于笼统,还是得深入到背后的执行原理才能举一反三解决问题。

举个简单的例子:

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

var foo = obj.foo;
var num = 3;

obj.foo();//2

foo();//3

obj.foo();是对象obj调用自身的属性(方法)foothis指向调用者obj
②由var foo = obj.foo;获取函数,并用foo();在全局作用域中调用函数,所以this指向浏览器全局对象Window

内存数据结构的概述

JS之所以设计this,是因为其内存的数据结构的特征。

var obj = { foo:  5 };

①上面的对象赋值给变量,其实质是JS引擎先在内存中生成{foo: 5},然后再把该对象的内存地址赋值给变量obj。也就是说onj.foo实质上是先从obj获取对象的内存地址然后再从地址读取原始对象,最后返回对象属性foo的值。
②原始对象以词典结构保存,一个属性(数据属性和存取属性: get&set)对应一个属性描述符对象。如下图,foo属性的描述对象就包含4个描述属性,而最重要的值保存在描述对象[[value]]中。
image
foo属性值为函数时,JS引擎同样会先将原始函数保存在内存中,然后把该函数的内存地址存放于foo属性的描述对象中的[[value]]里面。
image
属性描述符对象共有的可选键值:

可选键 键值 说明 数据属性 存取属性
configurable 布尔值,默认为 false 所有可选键是否可配置以及是否能删除该属性
enumerable 布尔值,默认为 false 是否可枚举
value JS有效值,默认为 undefined 该属性的值
writable 布尔值,默认为 false 是否可写
get getter函数,默认值 undefined 访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象。该函数的返回值会被用作属性的值
set setter函数,默认值 undefined 当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象

从原理出发章节参考于JavaScript 的 this 原理——阮一峰的网络日志

为何设计this

当我们我们需要 函数A 执行在 上下文环境B 中时就需要利用 this 为 A 指定执行环境 B,而不是又在 环境B 中再声明一边 函数A。就像这样:

var f = function (){
  console.log(this.x)
};

var x = 1;//全局变量 x
var obj = {
  x: 2,
  f: f
}

f();//结果为1,执行于全局环境,所以 this.x 指向 Window.x
obj.f();//结果为2,执行于 obj 内部环境,所以 this.x 指向 obj.x

由此可见,this是一个指针型变量,它指向当前函数的运行环境。

不同使用/调用情况下的this

在一个对象的方法中,this指向该对象。
直接单独使用,this指向全局对象。
在一个函数中,this指向全局对象(strict mode指向undefined)。
在JS事件中,this指向接受了该事件的元素对象
最后使用方法call(), apply(), bind()可以将this指向任何对象。

可能你会有疑问,为什么不直接说指向全局对象window,而仅说明指向全局对象。
这是因为在JS中全局对象也分三种情况:

  • 在浏览器中,JS没有专门用作后台任务启动的任何代码都将Window作为其全局对象。web应用绝大多数代码如此。
  • Worker中运行的代码将WorkerGlobalScope对象作为其全局对象。
  • Node.js环境下运行的脚本具有一个称为global的对象作为其全局对象。

注意:this不是一个JS变量而是指针型变量,它仅是访问函数执行环境的关键字,您不能更改它的值

this的扩展延伸

new关键字与this关键字

例1:

function name () {
  this.fne = "yulin"
};

var a = new name();
console.log(a.fne);//yulin

构造函数中的 this 指向:
①:首先new关键字会创建一个用户定义或者具有构造函数实例的内置对象。在上面的例子new关键字创建一个空对象name {}

②:然后构造函数name()this指向对象name {}内存地址,再执行name()并返回执行结果,所以空对象成为实例对象name {fine: 'yulin'}

③:最后整个对象name {fne: 'yulin'}赋值给变量a,变量a引用对实例象name,实质上是变量a取得了实例对象name的内存地址。

例2:MDN官方示例解析:

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);// 给 product() 指定一个调用 Food() 的 this 对象。
  this.category = 'food';
}

console.log(new Food('cheese', 5).name);// 构造一个 food() 实例,food() 内的 this 指向 food() 实例对象,所以 product() 变更 this 指向 food() 实例
// expected output: "cheese"

thisreturn的问题

上文说到构造函数执行的返回结果的有几种情况:

  1. 构造函数返回对象时,this会引用返回的对象,null除外,否则指向起始调用者。
function name (){
  this.fne = "yulin";
  return {}
};
var a = new name();
console.log(a.fne);//undefined,指向 {}

function name (){
  this.fne = "yulin";
  return function () {}
};
var a = new name();
console.log(a.fne);//undefined,指向 function () {}

function name (){
  this.fne = "yulin";
  return null
};
var a = new name();
console.log(a.fne);//yulin,指向 name {fne: 'yulin'},null是特殊的对象不会更改 this

function name () {
  this.fne = "yulin";
  return undefined
};
var a = new name();
console.log(a.fne);//yulin,指向 name {fne: 'yulin'}

指定this的值

  1. 当我们需要在指定的环境中执行一个函数时,我们就需要用到call()方法指定this的值,第一个参数为this的值,第二个参数是传入该函数的分隔参数。

    const a = {
      name: 'yulin',
      fn: function (e, q) {
      console.log(this.name);
      console.log(e + q);
      }
    };
    
    const b = {
      name: 'yhh'
    }
    
    var x = a.fn
    x.call(b, 2, 3);//yhh 5
    
  2. apply()方法与call()类似,但传入apply的第二个参数必须是数组或类数组对象(数组、集合、参数对象等等),就像[1, 2, 3], [a , b, c]以及数组变量。

    2.1 参数上的特性给数组对象带去了极大的便利:

    // 获取数组中最大值
    Math.max.apply(null, [1, 2, 3])// 3
    Math.max.apply(Math, [1, 2, 3])// 3
    Math.max.apply('', [1, 2, 3])// 3
    Math.max.apply(0 , [1, 2, 3])// 3
    
    // 用 apply 将数组各项添加到另一个数组
    const array = ['a', 'b'];
    const elements = [0, 1, 2];
    array.push.apply(array, elements);
    
    

注意:如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 JavaScript 引擎中表现不同。(JavaScriptCore 引擎中有被硬编码的参数个数上限:65536)。

2.2 如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法

function minOfArray(arr) {
  let min = Infinity;
  let QUANTUM = 32768; //最大切割长度,刚好是参数数量限定的最大值一半

  //数组长度小于32768只切割一次,大于32768且小于65536切割两次,大于65536小于(65536+32768)三次,以此类推
  for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
    const submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
    min = Math.min(submin, min);
  }

  return min;
}

let min = minOfArray([5, 6, 2, 3, 7]);

2.3 使用apply链接构造器
下面例子中我创建一个全局Function对象的construct方法 ,让构造器能够使用一个类数组对象参数而非参数列表。

// 给全局函数对象添加 construct 方法
Function.prototype.construct = function (aArgs) {
  const oNew = Object.create(this.prototype); // 目的是得到原型为 MyConstructor 的对象
  this.apply(oNew, aArgs);
  /* “原汤化原食”
  通过 apply 链接 this 指向的构造器函数,
  调用者是该构造器函数创建的对象实例
  */
  return oNew;
};

function MyConstructor() { // 传入不确定数量的参数,使用arguments无需arguments作为形参
  for (let nProp = 0; nProp < arguments.length; nProp++) {
    this['property' + nProp] = arguments[nProp];// this 指向的调用者对象更新属性
  }
}

let myArray = [4, 'Hello world!', false];
let myInstance = MyConstructor.construct(myArray);

console.log(myInstance.property1);                // logs 'Hello world!'
console.log(myInstance instanceof MyConstructor); // logs 'true'
console.log(myInstance.constructor);              // logs 'MyConstructor'

同理,我将construct方法稍加修改也能得到我预期的结果。

Function.prototype.construct = function (arr) {
  const oNew = Object.create({});// 以字面量对象 Object 为原型创建“白板”对象
  this.apply(oNew, arr);
  return oNew;
}

function MyConstructor() {
  let len = arguments.length
  for (let i=0; i<len; i++) {
    this['property' + i] = arguments[i];
  }
}

let myArray = [4, 'Hello world!', false];
let myInstance = MyConstructor.construct(myArray);

console.log(myInstance.property1);                // logs 'Hello world!'
console.log(myInstance instanceof MyConstructor); // logs 'false'
console.log(myInstance.constructor);              // logs 'Object'
  1. bind()创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
    3.1 普通使用情形
    const a = {
      name: 'yulin',
      fn: function (e, q) {
      console.log(this.name);
      console.log(e + q);
      }
    };
    
    const b = {
      name: 'yhh'
    }
    
    var x = a.fn
    
    x.bind(b);
    /*ƒ (e, q) {
      console.log(this.name);
      console.log(e + q);
    }*/
    
    let y = x.bind(b);
    y(2, 3);//yhh 5
    
    3.2 bind解决回调函数丢失this
    一个函数不论原先它的this指向谁。一旦作为回调函数(或定时回调)执行,它将遗忘原有的this并指向全局对象(严格模式同样适用)
    例子1:
    const person = {
      firstName:"John",
      lastName: "Doe",
      display: function () {
        console.log(this.firstName + " " + this.lastName);
      }
    }
    
    person.display();// 'John Doe'
    setTimeout(person.display, 3000);// undefined undefined
    
    例子2:
    var firstName = 'yu', lastName = 'lin';
    
    const person = {
      firstName:"John",
      lastName: "Doe",
      display: function () {
    	console.log(this.firstName + " " + this.lastName);
      }
    }
    
    person.display(); // 'John Doe'
    setTimeout(person.display, 3000); // 'yu lin'
    
    对于上面this的丢失,有两种方法可以解决:
    推荐使用箭头函数作为回调函数,箭头函数不会创建自己的this,它只会从自己的作用域链的父执行上下文继承this。并且作为回调函数不会丢失this值:
    var firstName = 'yu', lastName = 'lin';
    
    const person = {
      firstName:"John",
      lastName: "Doe",
      display: () => {
    	console.log(this.firstName + " " + this.lastName);
      }
    }
    
    let display = person.display.bind(person);// 返回 this 指向 person 的绑定函数 display
    setTimeout(display, 3000); // 'yu lin'
    
    使用函数bind方法绑定this
    var firstName = 'yu', lastName = 'lin';
    
    const person = {
      firstName:"John",
      lastName: "Doe",
      display: function () {
    	console.log(this.firstName + " " + this.lastName);
      }
    }
    
    let display = person.display.bind(person);// 返回 this 指向 person 的绑定函数 display
    setTimeout(display, 3000); // 'John Doe'
    
posted @ 2022-03-21 23:05  mx羽林  阅读(65)  评论(0编辑  收藏  举报