【THE LAST TIME】this:call、apply、bind

前言

The last time, I have learned

【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。javascript

也是给本身的查缺补漏和技术分享。html

欢迎你们多多评论指点吐槽。前端

系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见 Nealyang/personalBlog。目录皆为暂定

讲道理,这篇文章有些拿捏很差尺度。准确的说,这篇文章讲解的内容基本算是基础的基础了,可是每每这种基础类的文章很难在啰嗦和详细中把持好。文中道不到的地方还望各位评论多多补充指正。java

THE LAST TIME 系列

This

相信使用过 JavaScript 库作过开发的同窗对 this 都不会陌生。虽然在开发中 this 是很是很是常见的,可是想真正吃透 this,其实仍是有些不容易的。包括对于一些有经验的开发者来讲,也都要驻足琢磨琢磨~ 包括想写清楚 this 呢,其实还得聊一聊 JavaScript 的做用域和词法node

This 的误解一:this 指向他本身

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);

经过运行上面的代码咱们能够看到,foo函数的确是被调用了十次,可是this.count彷佛并无加到foo.count上。也就是说,函数中的this.count并非foo.countreact

This 的误解二:this 指向他的做用域

另外一种对this的误解是它不知怎么的指向函数的做用域,其实从某种意义上来讲他是正确的,可是从另外一种意义上来讲,这的确是一种误解。git

明确的说,this不会以任何方式指向函数的词法做用域,做用域好像是一个将全部可用标识符做为属性的对象,这从内部来讲他是对的,可是JavaScript代码不能访问这个做用域“对象”,由于它是引擎内部的实现es6

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined

全局环境中的 This

既然是全局环境,咱们固然须要去明确下宿主环境这个概念。简而言之,一门语言在运行的时候须要一个环境,而这个环境的就叫作宿主环境。对于 JavaScript 而言,宿主环境最为常见的就是 web 浏览器。github

如上所说,咱们也能够知道环境不是惟一的,也就是 JavaScript 代码不只仅能够在浏览器中跑,也能在其余提供了宿主环境的程序里面跑。另外一个最为常见的就是 Node 了,一样做为宿主环境node 也有本身的 JavaScript 引擎:v8.web

  • 浏览器中,在全局范围内,this 等价于 window 对象
  • 浏览器中,用 var 声明一个变量等价于给 this 或者 window 添加属性
  • 若是你在声明一个变量的时候没有使用var或者let(ECMAScript 6),你就是在给全局的this添加或者改变属性值
  • 在 node 环境里,若是使用 REPL 来执行程序,那么 this 就等于 global
  • 在 node 环境中,若是是执行一个 js 脚本,那么 this 并不指向 global 而是module.exports{}
  • 在node环境里,在全局范围内,若是你用REPL执行一个脚本文件,用var声明一个变量并不会和在浏览器里面同样将这个变量添加给this
  • 若是你不是用REPL执行脚本文件,而是直接执行代码,结果和在浏览器里面是同样的
  • node环境里,用REPL运行脚本文件的时候,若是在声明变量的时候没有使用var或者let,这个变量会自动添加到global对象,可是不会自动添加给this对象。若是是直接执行代码,则会同时添加给globalthis

这一块代码比较简单,咱们不用码说话,改成用图说话吧!

函数、方法中的 This

不少文章中会将函数和方法区分开,可是我以为。。。不必啊,咱就看谁点了如花这位菇凉就行

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

函数中的 this 是多变的,可是规则是不变的。

你问这个函数:”老妹~ oh,不,函数!谁点的你?“

”是他!!!“

那么,this 就指向那个家伙!再学术化一些,因此!通常状况下!this不是在编译的时候决定的,而是在运行的时候绑定的上下文执行环境。this 与声明无关!

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

记住上面说的,谁点的我!!! => foo() = windwo.foo(),因此其中this 执行的是 window 对象,天然而然的打印出来 2.

须要注意的是,对于严格模式来讲,默认绑定全局对象是不合法的,this被置为undefined。
function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

虽然这位 xx 被点的多了。。。可是,咱们只问点他的那我的,也就是 ojb2,因此 this.a输出的是 42.

注意,我这里的点!不是你想的那个点哦,是 运行时~

构造函数中的 This

恩。。。这,就是从良了

仍是如上文说到的,this,咱们不看在哪定义,而是看运行时。所谓的构造函数,就是关键字new打头!

谁给我 new,我跟谁

其实内部完成了以下事情:

  • 一个新的对象会被建立
  • 这个新建立的对象会被接入原型链
  • 这个新建立的对象会被设置为函数调用的this绑定
  • 除非函数返回一个他本身的其余对象,这个被new调用的函数将自动返回一个新建立的对象
foo = "bar";
function testThis(){
  this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行尝试

call、apply、bind 中的 this

恩。。。这就是被包了

在不少书中,call、apply、bind 被称之为 this 的强绑定。说白了,谁出力,我跟谁。那至于这三者的区别和实现以及原理呢,我们下文说!

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
dialogue.call(hero)//I am Batman

上面的dialogue.call(hero)等价于dialogue.apply(hero)`dialogue.bind(hero)()`.

其实也就是我明确的指定这个 this 是什么玩意儿!

箭头函数中的 this

箭头函数的 this 和 JavaScript 中的函数有些不一样。箭头函数会永久地捕获 this值,阻止 apply或 call后续更改它。

let obj = {
  name: "Nealyang",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); //   1 2
let func_ = func.bind(obj);
func_(1,2);//  1 2
func(1,2);//   1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);//  1 2

箭头函数内的 this值没法明确设置。此外,使用 call 、 apply或 bind等方法给 this传值,箭头函数会忽略。箭头函数引用的是箭头函数在建立时设置的 this值。

箭头函数也不能用做构造函数。所以,咱们也不能在箭头函数内给 this设置属性。

class 中的 this

虽然 JavaScript 是不是一个面向对象的语言至今还存在一些争议。这里咱们也不去争论。可是咱们都知道,类,是 JavaScript 应用程序中很是重要的一个部分。

类一般包含一个 constructor , this能够指向任何新建立的对象。

不过在做为方法时,若是该方法做为普通函数被调用, this也能够指向任何其余值。与方法同样,类也可能失去对接收器的跟踪。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();

构造函数里的 this指向新建立的 类实例。当咱们调用 batman.dialogue()时, dialogue()做为方法被调用, batman是它的接收器。

可是若是咱们将 dialogue()方法的引用存储起来,并稍后将其做为函数调用,咱们会丢失该方法的接收器,此时 this参数指向 undefined 。

const say = batman.dialogue;
say();

出现错误的缘由是JavaScript 类是隐式的运行在严格模式下的。咱们是在没有任何自动绑定的状况下调用 say()函数的。要解决这个问题,咱们须要手动使用 bind()将 dialogue()函数与 batman绑定在一块儿。

const say = batman.dialogue.bind(batman);
say();

this 的原理

咳咳,技术文章,我们严肃点

咱们都说,this指的是函数运行时所在的环境。可是为何呢?

咱们都知道,JavaScript 的一个对象的赋值是将地址赋值给变量的。引擎在读取变量的时候其实就是要了个地址而后再从原地址读出来对象。那么若是对象里属性也是引用类型的话(好比 function),固然也是如此!

截图自阮一峰博客

而JavaScript 容许函数体内部,引用当前环境的其余变量,而这个变量是由运行环境提供的。因为函数又能够在不一样的运行环境执行,因此须要个机制来给函数提供运行环境!而这个机制,也就是咱们说到心在的 this。this的初衷也就是在函数内部使用,代指当前的运行环境。

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

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

截图自阮一峰博客

obj.foo()是经过obj找到foo,因此就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数自己,因此foo()就变成在全局环境执行.

总结

  • 函数是否在new中调用,若是是的话this绑定的是新建立的对象
var bar = new Foo();
  • 函数是否经过call、apply或者其余硬性调用,若是是的话,this绑定的是指定的对象
var bar = foo.call(obj);
  • 函数是否在某一个上下文对象中调用,若是是的话,this绑定的是那个上下文对象
var bar = obj.foo();
  • 若是都不是的话,使用默认绑定,若是在严格模式下,就绑定到undefined,注意这里是方法里面的严格声明。不然绑定到全局对象
var bar = foo();

小试牛刀

var number = 2;
var obj = {
  number: 4,
  /*匿名函数自调*/
  fn1: (function() {
    var number;
    this.number *= 2; //4

    number = number * 2; //NaN
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2; //6
      console.log(num);
      number *= 3; //9
      alert(number);
    };
  })(),

  db2: function() {
    this.number *= 2;
  }
};

var fn1 = obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);
评论区留下你的答案吧~

call & applay

上文中已经提到了 callapplybind,在 MDN 中定义的 apply 以下:

apply() 方法调用一个函数, 其具备一个指定的this值,以及做为一个数组(或相似数组的对象)提供的参数

语法:

fun.apply(thisArg, [argsArray])
  • thisArg:在 fun 函数运行时指定的 this 值。须要注意的是,指定的 this 值并不必定是该函数执行时真正的 this 值,若是这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray:一个数组或者类数组对象,其中的数组元素将做为单独的参数传给 fun 函数。若是该参数的值为null 或 undefined,则表示不须要传入任何参数。从ECMAScript 5 开始可使用类数组对象。浏览器兼容性请参阅本文底部内容。

如上概念 apply 相似.区别就是 apply 和 call 传入的第二个参数类型不一样。

call 的语法为:

fun.call(thisArg[, arg1[, arg2[, ...]]])

须要注意的是:

  • 调用 call 的对象,必须是个函数 Function
  • call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。若是不传,则默认为全局对象 window。
  • 第二个参数开始,能够接收任意个参数。每一个参数会映射到相应位置的 Function 的参数上。可是若是将全部的参数做为数组传入,它们会做为一个总体映射到 Function 对应的第一个参数上,以后参数都为空。

apply 的语法为:

Function.apply(obj[,argArray])

须要注意的是:

  • 它的调用者必须是函数 Function,而且只接收两个参数
  • 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,而且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。

记忆技巧:apply,a 开头,array,因此第二参数须要传递数据。

请问!什么是类数组?

核心理念

借!

对,就是借。举个栗子!我没有女友,周末。。。额,不,我没有摩托车🏍,周末的时候天气很好,想出去压弯。可是我有没有钱!怎么办呢,找朋友借用一下啊~达到了目的,还节省开支!

放到程序中咱们能够理解为,某一个对象没有想用的方法去实现某个功能,可是不想浪费内存开销,就借用另外一个有该方法的对象去借用一下。

说白了,包括 bind,他们的核心理念都是借用方法,已达到节省开销的目的。

应用场景

代码比较简单,就不作讲解了
  • 将类数组转换为数组
const arrayLike = {
  0: 'qianlong',
  1: 'ziqi',
  2: 'qianduan',
  length: 3
}
const arr = Array.prototype.slice.call(arrayLike);

运行结果

  • 求数组中的最大值
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
  • 变量类型判断

Object.prototype.toString用来判断类型再合适不过,尤为是对于引用类型来讲。

function isArray(obj){
  return Object.prototype.toString.call(obj) == '[object Array]';
}
isArray([]) // true
isArray('qianlong') // false
  • 继承
// 父类
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; // 复杂类型
}
supFather.prototype.sayName = function (age) {
    console.log(this.name, 'age');
};
// 子类
function sub(name, age) {
    // 借用父类的方法:修改它的this指向,赋值父类的构造函数里面方法、属性到子类上
    supFather.call(this, name);
    this.age = age;
}
// 重写子类的prototype,修正constructor指向
function inheritPrototype(sonFn, fatherFn) {
    sonFn.prototype = Object.create(fatherFn.prototype); // 继承父类的属性以及方法
    sonFn.prototype.constructor = sonFn; // 修正constructor指向到继承的那个函数上
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
    console.log(this.age, 'foo');
};
// 实例化子类,能够在实例上找到属性、方法
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1) // {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2) // {"name":"小明","colors":["red","blue","green"],"age":18}
继承后面可能也会写一个篇【THE LAST TIME】。也是比较基础,不知道有没有这个必要

简易版继承

ar Person = function (name, age) {
  this.name = name;
  this.age = age;
};
var Girl = function (name) {
  Person.call(this, name);
};
var Boy = function (name, age) {
  Person.apply(this, arguments);
}
var g1 = new Girl ('qing');
var b1 = new Boy('qianlong', 100);

bind

bind 和 call/apply 用处是同样的,可是 bind返回一个新函数!不会当即执行!call/apply改变函数的 this 而且当即执行。

应用场景

  • 缓存参数
原理其实就是返回闭包,毕竟 bind 返回的是一个函数的拷贝
for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) // 依次输出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}

上述代码也是一个经典的面试题,具体也不展开了。

  • this 丢失问题

说道 this 丢失问题,应该最多见的就是 react 中定义一个方法而后后面要加 bind(this)的操做了吧!固然,箭头函数不须要,这个我们上面讨论过。

手写实现

apply

第一个手写我们一步一步来
  • 从定义触发,由于是 function 调用者。因此确定是给 function 添加方法咯,而且第一个参数是将来 this 上下文
Function.prototype.NealApply = function(context,args){}
  • 若是context,this 指向 window
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
}
  • 给 context 新增一个不可覆盖的 key,而后绑定 this
对,咱们没有黑魔法,既然绑定 this,仍是逃不掉咱们上文说的那些 this 方式
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //给context新增一个独一无二的属性以避免覆盖原有属性
    const key = Symbol();
    context[key] = this;//这里的 this 是函数
    context[key](...args);
}

其实这个时候咱们用起来已经有效果了。

  • 这个时候咱们已经执行完了,咱们须要将结果返回,而且清理本身产生的垃圾
Function.prototype.NealApply = function(context,args){
    context = context || window;
    args = args || [];
    //给context新增一个独一无二的属性以避免覆盖原有属性
    const key = Symbol();
    context[key] = this;//这里的 this 是 testFun
    const result = context[key](...args);
    // 带走产生的反作用
    delete context[key];
    return result;
}

var name = 'Neal'

function testFun(...args){
    console.log(this.name,...args);
}

const testObj = {
    name:'Nealyang'
}

testFun.NealApply(testObj,['一块儿关注',':','全栈前端精选']);

执行结果就是上方的截图。

  • 优化

一上来不说优化是由于但愿你们把精力放到核心,而后再去修边幅! 罗马不是一日建成的,看别人的代码多牛批,其实也是一点一点完善出来的。

道理是这么个道理,其实要作的优化还有不少,这里咱们就把 context 的判断须要优化下:

// 正确判断函数上下文对象
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }

别的优化你们能够添加各类的用户容错。好比对第二个参数的类数组作个容错

function isArrayLike(o) {
        if (o &&                                    // o不是null、undefined等
            typeof o === 'object' &&                // o是对象
            isFinite(o.length) &&                   // o.length是有限数值
            o.length >= 0 &&                        // o.length为非负值
            o.length === Math.floor(o.length) &&    // o.length是整数
            o.length < 4294967296)                  // o.length < 2^32
            return true;
        else
            return false;
    }
打住!真的再也不多啰嗦了,这篇文章篇幅不该这样的

call

丐版实现:

//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也能够用arguments代替
Function.prototype.NealCall = function (context, ...args) {
    //这里默认不传就是给window,也能够用es6给参数设置默认参数
    context = context || window;
    args = args ? args : [];
    //给context新增一个独一无二的属性以避免覆盖原有属性
    const key = Symbol();
    context[key] = this;
    //经过隐式绑定的方式调用函数
    const result = context[key](...args);
    //删除添加的属性
    delete context[key];
    //返回函数调用的返回值
    return result;
}

bind

bind的实现讲道理是比 apply 和call 麻烦一些的,也是面试频考题。由于须要去考虑函数的拷贝。可是也仍是比较简单的,网上也有不少版本,这里就不具体展开了。具体的,我们能够在群里讨论~

Function.prototype.myBind = function (objThis, ...params) {
    const thisFn = this; // 存储源函数以及上方的params(函数参数)
    // 对返回的函数 secondParams 二次传参
    let fToBind = function (...secondParams) {
        const isNew = this instanceof fToBind // this是不是fToBind的实例 也就是返回的fToBind是否经过new调用
        const context = isNew ? this : Object(objThis) // new调用就绑定到this上,不然就绑定到传入的objThis上
        return thisFn.call(context, ...params, ...secondParams); // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    };
    if (thisFn.prototype) {
        // 复制源函数的prototype给fToBind 一些状况下函数没有prototype,好比箭头函数
        fToBind.prototype = Object.create(thisFn.prototype);
    }
    return fToBind; // 返回拷贝的函数
};
Function.prototype.myBind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}

最后

别忘记了上面 this 的考核题目啊,同窗,该交卷了!

参考连接

学习交流

关注公众号: 【全栈前端精选】 每日获取好文推荐。还能够入群,一块儿学习交流