注: 本文案例环境为非严格模式, 严格模式下禁止关键字 this 指向全局对象
一方法是怎么执行的?
首先说一下 js 中方法的执行, 在 window 全局下声明一个方法 a:
- function a () {console.log(this);
- }
- a();//window
全局中执行这个方法普遍的方法是直接 a(), 控制台会打印出 window 对象
那么为什么会打印出 window 对象呢? 我们可以这样理解, 方法的执行必须要有个调用对象, 刚才那个方法 a 是定义在 window 全局下的, window 下的变量和方法有个特点就是访问和调用的时候可以省略 window! 所以刚才执行 a() === window.a(), 也就是说, 执行 a 方法时的直接调用者是 window 所以打印出的是 window!
上面有提到直接调用者, 什么是方法的直接调用者呢? 举个例子, 声明一个全局对象 obj:
- var name = "window-name";
- var obj = {
- name: "obj-name",
- a: function() {
- console.log(this.name);
- },
- b: {
- name: "b-name",
- a: function() {
- console.log(this.name);
- }
- }
- }
- obj.a(); //obj-name
- obj.b.a(); //b-name
分别执行 obj.a(); 和 obj.b.a(); 控制台会分别打印出 obj-name 和 b-name(这里 obj.a() === window.obj.a(),obj.b.a() === window.obj.b.a()), 执行这两个 a()的直接调用者分别是谁呢? 方法的直接调用者就是离这个被调用方法最近的那个对象, 两个分别是 obj 和 b, 所以打印出的 name 分别是 obj.name 和 b.name
二 this 指向了谁?
那么函数里面的 this 到底是谁呢? this 就是这个方法被调用时的直接调用者在刚才的基础上定义一个全局变量:
- var ax = obj.b.a;
- ax();//window-name
此时执行 ax(); 控制台则会打印出 window-name; 为什么会打印出 window-name? 这是因为 ax 是定义在 window 全局下的变量, 执行 ax()时的直接调用者是 window(ax() === window.ax()), 所以执行 ax()时内部的 this 就是它的直接调用者 window, 因此打印出的值就是定义在 window 下的 name 的值
三 call 和 apply 改变了什么?
理解了函数的直接调用者和 this, 再说 call 和 apply 就比较容易理解了 在此对 call 和 apply 不做过多的定义性解释, 先来看下调用了 call 后谁是那个被执行的方法, 直接代码示例:
- function fn1 () {
- console.log(1);
- };
- function fn2 () {
- console.log(2);
- };
- fn1.call(fn2);//1
执行 fn1.call(fn2); 控制台会打印 1, 这里可以说明 fn1 调用 call 后被执行的方法还是 fn1 一定要弄清楚谁是这个被执行的方法, 就是调用 call 的函数, 而 fn2 现在的身份是替代 window 作为 fn1 的直接调用者, 这是理解 call 和 apply 的关键, 也可以运行下 fn2.call(fn1);//2 来验证被执行的方法是谁那么 call 的作用是什么呢? 再来个代码示例:
- var obj1 = {
- num: 20,
- fn: function(n) {
- console.log(this.num + n);
- }
- };
- var obj2 = {
- num: 15,
- fn: function(n) {
- console.log(this.num - n);
- }
- };
- obj1.fn.call(obj2, 10); //25
执行 obj1.fn.call(obj2,10); 控制台会打印 25,call 在此的作用其实很简单, 就是在执行 obj1.fn 的时候把这个 fn 的直接调用者由 obj1 变为 obj2,obj1.fn(n)内部的 this 经过 call 的作用指向了 obj2, 所以 this.num 就是 obj2.num,10 作为执行 obj1.fn 时传入的参数, obj.num 是 15, 因此打印出的值是 15+10=25
所以我们可以这样理解: call 的作用是改变了那个被执行的方法 (也就是调用 call 的那个方法) 的直接调用者! 而这个被执行的方法内部的 this 也会重新指向那个新的调用者, 就是 call 方法所接收的第一个 obj 参数
四 call 和 apply 的区别
call 方法除了第一个 obj 参数外, 还接受一串参数作为被执行的方法的参数, apply 用法和 call 类似, 只不过除第一个 obj 参数外, 接收的第二个参数是一个数组来作为被执行的方法的参数
五延伸拓展
我们来执行下面的代码:
fn1.call.call(fn2);//2
执行 fn1.call.call(fn2); 控制台会打印出 2, 先不说为什么会打印出 2, 先来理解下 fn1.call.call 是什么, call()方法是 Function 对象原型链上的方法, 所以 fn1 这个函数可以通过原型链继承使用这个方法, 也就是说 fn1.call === Function.prototype.call === Function.call 所以 fn1.call.call(fn2) === Function.call.call(fn2), 可以把 Function.call 先看做一个整体, 用 FunCall 来表示如下:
FunCall.call(fn2);
这样就比较好理解, 就是 fn2 作为 FunCall 的直接调用者来执行 FunCall, 相当于 fn2 作为直接调用者执行了 FunCall(), 而 FunCall === Function.call, 所以就相当于是 fn2.call()
此时 call 没有传入对象, 那么全局对象 window 就会作为默认对象, 也就是相当于 fn2.call(window), 再继续解释就是 window.fn2.call(window), 把 fn2 的直接调用对象由 window 改变成 window, 相当于没有改变 fn2 的直接调用对象, 所以就相当于直接执行了 fn2(); 控制台会打印出 2
此外还有 Function.call.apply 和 Function.apply.call 等多种组合, 原理都类似, 只不过接收的参数类型不太一样, 可以尝试一下加深对 call 和 apply 的理解
来源: https://juejin.im/post/5aab40bef265da23826dba61