this指向详解思维脑图与代码的结合让你一篇搞懂thiscallapply系列一

52次阅读

共计 6311 个字符,预计需要花费 16 分钟才能阅读完成。

this 指向详解

这是我在 segmentfault 的第一篇文章,欢迎大家指正
思考 + 导图 + 示例代码 => 船新版本

目录

  • 前言 + 思考题
  • 一、this 的指向
  • 二、call 和 apply
  • 三、模拟实现一个 call
  • 四、bind
  • 五、结尾

前言 + 思考题

记得当时找实习的时候,总是会在简历上加上一句——熟悉 Js,例如 this 指向、call、apply 等 …

而每次投递简历时我都会经历如下步骤

  • 面试前,去问度娘——this 指向可以分为哪几种啊~、call 和 apply 的区别是什么?底气由 0% 猛涨到了 50%;
  • 面试中,面试官随便扔上来几道题,我都可以“坚定的”给出答案,结果总是不尽人意 …
  • 面试后,我会羞愧的删除掉简历上的这一条。而再之后投递简历时我又再次加上了这一条 …

思考题

下面几道题是我在网上搜索出来的热度较高的问题,如果大佬们可以轻松的回答上,并有清晰的思路,不妨直接点个赞吧(毕竟也消耗了不少脑细胞),如果大佬们能在评论处指点一二,就更好了!!!

填空题:

  • 执行 Javascript 中的 【】 函数会创建一个新函数,新函数与被调函数具有相同的函数体,当目标函数被调用时 this 值指向第一个参数。

问答题:

  • 请你谈一下改变函数内部 this 指针的指向函数有哪几种,他们的区别是什么?
  • this 的指向可以分为哪几种?

代码分析题:

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {return function () {console.log(this.name)
    }
  },
  show4: function () {return () => console.log(this.name)
  }
}
var person2 = {name: 'person2'}

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

一、this 的指向

百度、谷歌上输入“this 的指向”关键字,大几千条文章肯定是有的,总不至于为了全方面、无死角的掌握它就要将所有的文章都看一遍吧?所以不如梳理出一个稳固的框架,顺着我们的思路来填充它。

思维导图

本节精华:

  • this 总是(非严格模式下)指向一个对象,而具体指向哪个对象是在运行时基于函数的 执行环境 动态绑定的,而非函数被声明时的环境;
  • 除了不常用的 with 和 eval 的情况,具体到实际应用中,this 指向大概可以分为四种:

    • 作为对象的方法调用;
    • 作为普通函数调用;
    • 构造器调用;
    • call 或 apply 调用;
    • 箭头函数中,this 指向函数上层作用域的 this;
  • 构造器 普通函数 的区别在于 被调用的方式
  • A,call(B) => 可以理解成在 B 的作用域内调用了 A 方法;

分析

1、作为对象的方法调用

当函数作为对象的方法被调用时,this 指向该对象

var obj = {
    a: 'yuguang',
    getName: function(){console.log(this === obj);
        console.log(this.a);
    }
};

obj.getName(); // true yuguang

2、作为普通函数调用

当函数不作为对象的属性被调用,而是以普通函数的方式,this 总是指向全局对象(在浏览器中,通常是 Window 对象)

window.name = 'yuguang';

var getName = function(){console.log(this.name);
};

getName(); // yuguang

或者下面这段迷惑性的代码:

window.name = '老王'
var obj = {
    name: 'yuguang',
    getName: function(){console.log(this.name);
    }
};

var getNew = obj.getName;
getNew(); // 老王

而在 ES5 的严格模式下,this 被规定为不会指向全局对象,而是undefined

3、构造器调用

除了一些内置函数,大部分 Js 中的函数都可以成为构造器,它们与普通函数没什么不同

构造器 普通函数 的区别在于 被调用的方式
当 new 运算符调用函数时,总是返回一个对象,this 通常也指向这个对象

var MyClass = function(){this.name = 'yuguang';}
var obj = new MyClass();
obj.name; // yuguang

但是,如果显式的返回了一个 object 对象,那么此次运算结果最终会返回这个对象。

var MyClass = function () {
    this.name = 1;
    return {name: 2}
}
var myClass = new MyClass(); 
console.log('myClass:', myClass); // {name: 2}

只要构造器不显示的返回任何数据,或者返回非对象类型的数据,就不会造成上述问题。

4、call 或 apply 调用

跟普通的函数调用相比,用 call 和 apply 可以动态的改变函数的 this

var obj1 = {
    name: 1,
    getName: function (num = '') {return this.name + num;}
};

var obj2 = {name: 2,};
// 可以理解成在 obj2 的作用域下调用了 obj1.getName()函数
console.log(obj1.getName()); // 1
console.log(obj1.getName.call(obj2, 2)); // 2 + 2 = 4
console.log(obj1.getName.apply(obj2, [2])); // 2 + 2 = 4

5. 箭头函数

箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。

因此,在下面的代码中,传递给 setInterval 的函数内的 this 与封闭函数中的 this 值相同:

this.val = 2;
var obj = {
    val: 1,
    getVal: () => {console.log(this.val);
    }
}

obj.getVal(); // 2

常见的坑

就像标题一样,有的时候 this 会指向 undefined

情况一

var obj = {
    name: '1',
    getName: function (params) {console.log(this.name)
    }
};
obj.getName();

var getName2 = obj.getName;
getName2();

这个时候,getName2()作为普通函数被调用时,this 指向全局对象——window。

情况二

当我们希望自己封装 Dom 方法,来精简代码时:

var getDomById = function (id) {return document.getElementById(id);
};
getDomById('div1') //dom 节点

那么我们看看这么写行不行?

var getDomById = document.getElementById
getDomById('div1') // Uncaught TypeError: Illegal invocation(非法调用)

这是因为:

  • 当我们去调用 document 对象的方法时,方法内的 this 指向document
  • 当我们用 getId 应用 document 内的方法,再以普通函数的方式调用,函数内容的 this 就指向了全局对象。

利用 call 和 apply 修正情况二

document.getElementById = (function (func) {return function(){return func.call(document, ...arguments)
    }
})(document.getElementById) 
// 利用立即执行函数将 document 保存在作用域中

二、call 和 apply

不要因为它的“强大”而对它产生抗拒,了解并熟悉它是我们必须要做的,共勉!

思维导图

1.call 和 apply 区别

先来看区别,是因为它们 几乎 没有区别,下文代码实例 call 和 apply 都可以轻易的切换。

当它们被设计出来时要做到的事情一摸一样,唯一的区别就在于 传参的格式不一样

  • apply 接受两个参数

    • 第一个参数指定了函数体内 this 对象的指向
    • 第二个参数为一个带下标的参数集合(可以是数组或者类数组)
  • call 接受的参数不固定

    • 第一个参数指定了函数体内 this 对象的指向
    • 第二个参数及以后为函数调用的参数

因为在所有(非箭头)函数中都可以通过 arguments 对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,它本身就是一个类数组,我们 apply 在实际使用中更常见一些。

call 是包装在 apply 上面的语法糖,如果我们明确的知道参数数量,并且希望展示它们,可以使用 call。

当使用 call 或者 apply 的时候,如果我们传入的第一个参数为 null,函数体内的 this 会默认指向宿主对象,在浏览器中则是window

借用其他对象的方法

我们可以直接传 null 来代替任意对象

Math.max.apply(null, [1, 2, 3, 4, 5])

2.call 和 apply 能做什么?

使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数——来时 MDN

  • 调用构造函数来 实现继承;
  • 调用函数并且指定上下文的 this;
  • 调用函数并且不指定第一个参数;

1. 调用构造函数来实现继承

通过“借用”的方式来达到继承的效果:

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

function Food(name, price) {Product.call(this, name, price); //
    this.category = food;
}

var hotDog = new Food('hotDog', 20);

2. 调用函数并且指定上下文的 this

此时 this 被指向了 obj

function showName() {console.log(this.id + ':' + this.name);
};

var obj = {
    id: 1,
    name: 'yuguang'
};

showName.call(obj)

3. 使用 call 单纯的调用某个函数

Math.max.apply(null, [1,2,3,10,4,5]); // 10

三、模拟实现一个 call

先来看一下 call 帮我们需要做什么?

var foo = {value: 1};
function show() {console.log(this.value);
};
show.call(foo); //1

就像解方程,要在已知条件中寻找突破哦口:

  • call 使得 this 的指向变了,指向了 foo;
  • show 函数被执行了;
  • 传入的参数应为 this + 参数列表;

第一版代码

上面提到的 3 点,仅仅完成了一点,且传入的参数

var foo = {value: 1};
function show() {console.log(this.value);
};
Function.prototype.setCall = function (obj) {console.log(this); // 此时 this 指向 show
    obj.func = this; // 将函数变成对象的内部属性
    obj.func(obj.value); // 指定函数
    delete obj.func // 删除函数,当做什么都没发生~
}
show.setCall(foo);

第二版代码

为了解决参数的问题,我们要能获取到参数,并且正确的传入:

var foo = {value: 1};
function show(a, b) {console.log(this.value);
    console.log(a + b);
};
Function.prototype.setCall = function (obj) {
    obj.fn = this; // 将函数变成对象的内部属性
    var args = [];
    for(let i = 1; i < arguments.length; i++){args.push('arguments[' + i + ']');
    }
    eval('obj.fn(' + args + ')'); // 传入参数
    delete obj.fn; // 删除函数,当做什么都没发生~
}

show.setCall(foo, 1, 2); // 1 3

此时,我们就可以做到,传入多个参数的情况下使用 call 了,但是如果你仅想用某个方法呢?

第三版代码

Function.prototype.setCall = function (obj) {
    var obj = obj || window;
    obj.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {args.push('arguments[' + i + ']');
      }
      var result = eval('obj.fn(' + args +')');
      delete obj.fn;
      return result;
};
// 测试一下
var value = 2;
var obj = {value: 1};

function bar(name, age) {console.log(this.value);
      return {
        value: this.value,
        name: name,
        age: age
      }
}
bar.setCall(null); // 2
console.log(bar.setCall(obj, 'yuguang', 18));

四、bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用 —— MDN

提到了 callapply,就绕不开bind。我们试着来模拟一个 bind 方法,以便加深我们的认识:

Function.prototype.bind = function (obj) {
    var _this = this; // 保存调用 bind 的函数
    var obj = obj || window; // 确定被指向的 this,如果 obj 为空,执行作用域的 this 就需要顶上喽
    return function(){return _this.apply(obj, arguments); // 修正 this 的指向
    }
};

var obj = {
    name: 1,
    getName: function(){console.log(this.name)
    }
};

var func = function(){console.log(this.name);
}.bind(obj);

func(); // 1

这样看上去,返回一个原函数的拷贝,并拥有指定的 this 值,还是挺靠谱的哦~

写在最后

JavaScript 内功基础部分第一篇,总结这个系列是受到了冴羽大大的鼓励和启发,本系列总章数待定,保证都是我们在面试最高频的,但在工作中常常被忽略的。

JavaScript 内功系列:

  1. 本文
  2. 下一篇预发:原型和原型链

关于我

  • 花名:余光
  • 前端开发一枚,水平有限,虚心学习中

其他沉淀

  • JavaScript 版 LeetCode 题解
  • 前端进阶笔记

如果您看到了最后,不妨收藏、点赞、评论一下吧!!!
持续更新,您的三连就是我最大的动力,虚心接受大佬们的批评和指点,共勉!

正文完
 0