3

全面解析JavaScript中this指向问题

 3 years ago
source link: https://zhuanlan.zhihu.com/p/125637186
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

全面解析JavaScript中this指向问题

Enjoy what you are doing!

this指向

参考文章:
* this

JavaScriptthis指向分为以下几种情况:

  • 普通函数或作为对象属性
  • call/apply/bind指定

下面我们来进行一一介绍

普通函数或作为对象属性

this取决于方法执行前面是否有“点”,有“点”的话,“点”前面是谁this就是谁,如果没有点的话,this指向window

const fn = function () {
  console.log(this);
};

const obj = { name: 'OBJ', fn };

fn();

obj.fn();

const fn1 = obj.fn;
fn1();

answer:

1. window
2. {name: 'OBJ', fn: function() {console.log(this)}} // obj
3. window

可以看到函数作为对象的属性被调用的时候,其this指向调用该函数的对象,否则其this指向window

在进行事件绑定的时候,事件绑定函数中的this是绑定事件的元素:

// 假设页面中有id为button的button元素
// var x = 100;
window.x = 100;
const fn = function () {
  console.log(this.x);
};
const obj = { x: 200, fn };
const $button = document.getElementById('button');
$button.x = 300;

obj.fn();
const fn1 = obj.fn;
fn1();

$button.addEventListener('click', fn);
$button.addEventListener('mouseenter', obj.fn);

$button.addEventListener('mouseleave', function () {obj.fn();});

answer:

1. 200
2. 100
3. 点击button时:300
4. 鼠标移入button时:300
5. 鼠标移出时:200

但是需要注意的是,这里我们是在用户点击时,浏览器帮我们将点击事件的this指向绑定该事件的DOM元素。如果通过代码来触发对应的事件的话,我们可以通过call/apply/bind来指定其this

$button.click.call() // this为window,打印结果为100

构造函数(new Fn)

构造函数(new Fn)执行,函数中的this是当前类的实例,这是new关键字帮我们做到的:

var x = 100;
const Fn = function () {
  this.x = 200;
  console.log(this.x);
};

const fn = new Fn();

answer:

1. 200

箭头函数中没有自身的this,所用到的this都是其最近父级上下文中的this

const fn = function () {
  console.log(this);
  setTimeout(() => {
    console.log(this);
  }, 1000);
  setTimeout(function () {
    console.log(this);
  });
};

const obj = { x: 100, fn };

obj.fn();

answer:

1. {x:100, fn: function() {...}} // obj
2. window
3. {x:100, fn: function() {...}} // obj

call/apply/bind改变this指向

call/apply/bind传入的第一个参数即为函数的this

var x = 100;
const obj = { x: 200, y: 200 };
const fn = function () {
  console.log(this.x);
};

fn();
fn.call(obj);
fn.apply(obj);

const fixedThisFn = fn.bind(obj);
fixedThisFn();

answer:

1. 100
2. 200
3. 200
4. 200
  • call在执行时,第一个参数为this指向,之后的参数为fn执行时的参数
  • apply在执行时,第一个参数为this指向,之后的参数为fn执行时的参数组成的数组,数组的每一项会和fn的每一个参数进行对应
  • bind在执行时,第一个参数为预先传入this指向,之后的参数为实际调用fn前预先传入的参数,返回值为一个函数fixedThisFnfixedThisFn内部会调用fn并指定其this指向

为了更深入的理解call/apply/bind是如何改变函数中this指向的,下面我们分别模拟实现这三个函数

call/apply/bind源码实现

根据前面的介绍,我们知道:当函数作为对象属性被调用时,this指向调用该函数的对象

const obj = { x: 100, fn () {console.log(this);} };
obj.fn(); // {x: 100, fn: function() {...}} => obj

利用JavaScript这个特性,我们可以将执行的函数作为call/apply的第一个参数context的属性,然后通过context来调用该属性对应的函数,函数的this便指向了context

call的源码模拟如下:

Function.prototype.myOwnCall = function (context, ...args) {
  const uniqueKey = new Date().getTime();
  // this为调用call方法的函数
  context[uniqueKey] = this;
  // 作为对象的方法被对象调用,this指向该对象context
  const result = context[uniqueKey](...args);
  delete context[uniqueKey];
  return result;
};

到这里,有的小伙伴可能已经发现了,如果call/apply传入的context不是对象呢?

首先我们看下mdncall方法的第一个参数的描述:

语法:function.call(thisArg, arg1, arg2, ...)
* thisArg
可选的。在function函数运行时使用的this值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定nullundefined时会自动替换为指向全局对象,原始值会被包装

接下来,我们对myOwnCall方法的第一个参数做如下处理:

function translateToObject (context) {
  // 可以通过 == 进行判断 context == null
  // null == undefined  => 2个等号是成立的
  // null,undefined => window
  if (typeof context === 'undefined' || context === null) {
    context = window;
  } else if (typeof context === 'number') { // 原始值转换为包装对象
    context = new Number(context);
  } else if (typeof context === 'string') {
    context = new String(context);
  } else if (typeof context === 'boolean') {
    context = new Boolean(context);
  }
  return context;
}

myOwnCall方法中调用该函数:

Function.prototype.myOwnCall = function (context, ...args) {
  context = translateToObject(context);
  const uniqueKey = new Date().getTime();
  // this为调用call方法的函数
  context[uniqueKey] = this;
  // 作为对象的方法被对象调用,this指向该对象context
  const result = context[uniqueKey](...args);
  delete context[uniqueKey];
  return result;
};

apply的实现与call基本相同,只不过第二个参数是一个数组:

Function.prototype.myOwnBind = function (context, paramsArray) {
  context = translateToObject(context);
  const uniqueKey = new Date().getTime();
  // this为调用call方法的函数
  context[uniqueKey] = this;
  // 作为对象的方法被对象调用,this指向该对象context
  const result = context[uniqueKey](...paramsArray);
  delete context[uniqueKey];
  return result;
};

相比于call/applybind函数并没有立即执行函数,而是预先传入函数执行时的this和参数,并且返回一个函数,在返回的函数中执行调用bind函数并将预先传入的this和参数传入

bind的源码模拟:

Function.prototype.myOwnBind = function (context, ...outerArgs) {
  const fn = this;
  return function (...innerArgs) {
    return fn.call(context, ...outerArgs, ...innerArgs);
  };
};

精简版如下:

Function.prototype.myOwnBind = (context, ...outerArgs) => (...innerArgs) => this.call(context, ...outerArgs, ...innerArgs);

这里并没有实现通过new操作符来执行fn.bind(context)的操作,如果想知道其详细的实现过程,可以看我的这篇文章: JS进阶-手写bind

在深入理解call/apply/bind的实现原理后,我们尝试完成下面的测试:

function fn1 () {console.log(1);}
function fn2 () {console.log(2);}
fn1.call(fn2);

fn1.call.call(fn2);

Function.prototype.call(fn1);
Function.prototype.call.call(fn1);

answer:

1. 1
2. 2
3. 什么都不输出
4. 1

这里我们根据call的源码来进行推导一下Function.prototype.call.call(fn1),其它的执行过程类似:

// 1. 首先会将Function.prototype.call作为一个函数来执行它原型上的call方法
// 所以call方法内部:
//    this => Function.prototype.call
//    context => fn1
// 通过对象的属性来执行方法改变this指向
//    fn1[uniqueKey] = this(Function.prototype.call)
//    fn1[uniqueKey]() // 执行 Function.prototype.call方法,但是this是context
// 2. 在this为fn1的情况下执行Function.prototype.call方法
// 所以call方法内部:
//    this => fn1
//    context => window
// 通过对象的属性来改变this指向
//    window[uniqueKey] = fn1
//    window[uniqueKey]() // 执行fn1(),但是this是window

这里就是有笔者关于JavaScriptthis指向的相关内容的理解,希望能对阅读的小伙伴有所帮助


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK