JS 系列五:深刻 call、apply、bind

引言

JS系列暂定 27 篇,从基础,到原型,到异步,到设计模式,到架构模式等。javascript

本篇是JS系列中第 5 篇,文章主讲 JS 中 call 、 applybind 、箭头函数以及柯里化,着重介绍它们之间的区别、对比使用,深刻了解 call 、 applybind前端

1、Function.prototype.call()

call() 方法调用一个函数, 其具备一个指定的 this 值和多个参数(参数的列表)。java

func.call(thisArg, arg1, arg2, ...)
复制代码

它运行 func,提供的第一个参数 thisArg 做为 this,后面的做为参数。git

1. func 与 func.call

先看一个例子:github

func(1, 2, 3);
func.call(obj, 1, 2, 3)
复制代码

他们都调用的是 func,参数是 12 和 3设计模式

惟一的区别是 func.call 也将 this 设置为 obj数组

须要注意的是,设置的 thisArg 值并不必定是该函数执行时真正的 this 值,若是这个函数处于非严格模式下,则指定为 nullundefinedthis 值会自动指向全局对象(浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。浏览器

2. func.call 绑定上下文

例如,在下面的代码中,咱们在对象的上下文中调用 sayWord.call(bottle) 运行 sayWord ,并 bottle 传递为 sayWordthis缓存

function sayWord() {
  var talk = [this.name, 'say', this.word].join(' ');
  console.log(talk);
}

var bottle = {
  name: 'bottle', 
  word: 'hello'
};

// 使用 call 将 bottle 传递为 sayWord 的 this
sayWord.call(bottle); 
// bottle say hello
复制代码

3. 使用 func.call 时未指定 this

非严格模式
// 非严格模式下
var bottle = 'bottle'
function say(){
   // 注意:非严格模式下,this 为 window
   console.log('name is %s',this.bottle)
}

say.call()
// name is bottle
复制代码
严格模式
// 严格模式下
'use strict'
var bottle = 'bottle'
function say(){
   // 注意:在严格模式下 this 为 undefined
   console.log('name is %s',this.bottle)
}

say.call()
// Uncaught TypeError: Cannot read property 'bottle' of undefined
复制代码

4. call 在 JS 继承中的使用: 构造继承

基本思想:在子类型的构造函数内部调用父类型构造函数。闭包

注意:函数只不过是在特定环境中执行代码的对象,因此这里使用 apply/call 来实现。

使用父类的构造函数来加强子类实例,等因而复制父类的实例属性给子类(没用到原型)

// 父类
function SuperType (name) {
  this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
  return this.name;
};

// 子类
function SubType () {
  // 调用 SuperType 构造函数
  // 在子类构造函数中,向父类构造函数传参
  SuperType.call(this, 'SuperType'); 
  // 为了保证子父类的构造函数不会重写子类的属性,须要在调用父类构造函数后,定义子类的属性
  this.subName = "SubType"; 
  // 子类属性
};

// 子类实例
let instance = new SubType(); 
// 运行子类构造函数,并在子类构造函数中运行父类构造函数,this绑定到子类
复制代码

5. 解决 var 做用域问题

var bottle = [
  {name: 'an', age: '24'},
  {name: 'anGe', age: '12'}
];

for (var i = 0; i < bottle.length; i++) {
  // 匿名函数
  (function (i) { 
    setTimeout(() => {
      // this 指向了 bottle[i]
      console.log('#' + i  + ' ' + this.name + ': ' + this.age); 
    }, 1000)
  }).call(bottle[i], i);
  // 调用 call 方法,同时解决了 var 做用域问题
}
复制代码

打印结果:

#0 an: 24
#1 anGe: 12
复制代码

在上面例中的 for 循环体内,咱们建立了一个匿名函数,而后经过调用该函数的 call 方法,将每一个数组元素做为指定的 this 值当即执行了那个匿名函数。这个当即执行的匿名函数的做用是打印出 bottle[i] 对象在数组中的正确索引号。

2、Function.prototype.apply()

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

func.apply(thisArg, [argsArray])
复制代码

它运行 func 设置 this = context 并使用类数组对象 args 做为参数列表。

例如,这两个调用几乎相同:

func(1, 2, 3);
func.apply(context, [1, 2, 3])
复制代码

两个都运行 func 给定的参数是 1,2,3。可是 apply 也设置了 this = context

call 和 apply 之间惟一的语法区别是 call 接受一个参数列表,而 apply 则接受带有一个类数组对象。

须要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。若是传入类数组对象,它们会抛出异常。

1. call、apply 与 扩展运算符

咱们已经知道了JS 基础之: var、let、const、解构、展开、函数 一章中的扩展运算符 ...,它能够将数组(或任何可迭代的)做为参数列表传递。所以,若是咱们将它与 call 一块儿使用,就能够实现与 apply 几乎相同的功能。

这两个调用结果几乎相同:

let args = [1, 2, 3];

func.call(context, ...args); // 使用 spread 运算符将数组做为参数列表传递
func.apply(context, args);   // 与使用 call 相同
复制代码

若是咱们仔细观察,那么 callapply 的使用会有一些细微的差异。

  • 扩展运算符 ... 容许将 可迭代的 参数列表 做为列表传递给 call
  • apply 只接受 类数组同样的 参数列表

2. apply 函数转移

apply 最重要的用途之一是将调用传递给另外一个函数,以下所示:

let wrapper = function() {
  return anotherFunction.apply(this, arguments);
};
复制代码

wrapper 经过 anotherFunction.apply 得到了上下文 thisanotherFunction 的参数并返回其结果。

当外部代码调用这样的 wrapper 时,它与原始函数的调用没法区分。

3. apply 链接数组

array.push.apply 将数组添加到另外一数组上:

var array = ['a', 'b']
var elements = [0, 1, 2]
array.push.apply(array, elements)
console.info(array) // ["a", "b", 0, 1, 2]
复制代码

4. apply 来连接构造器

Function.prototype.constructor = function (aArgs) {
  var oNew = Object.create(this.prototype);
  this.apply(oNew, aArgs);
  return oNew;
};
复制代码

5. apply 和内置函数

/* 找出数组中最大/小的数字 */
let numbers = [5, 6, 2, 3, 7]
/* 应用(apply) Math.min/Math.max 内置函数完成 */

let max = Math.max.apply(null, numbers) 
/* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */

let min = Math.min.apply(null, numbers)

console.log('max: ', max)
// max: 7
console.log('min: ', min)
// min: 2
复制代码

它至关于:

/* 代码对比: 用简单循环完成 */
let numbers = [5, 6, 2, 3, 7]
let max = -Infinity, min = +Infinity
for (var i = 0; i < numbers.length; i++) {
  if (numbers[i] > max)
    max = numbers[i]
  if (numbers[i] < min) 
    min = numbers[i]
}

console.log('max: ', max)
// max: 7
console.log('min: ', min)
// min: 2
复制代码

可是:若是用上面的方式调用 apply,会有超出 JavaScript 引擎的参数长度限制的风险。更糟糕的是其余引擎会直接限制传入到方法的参数个数,致使参数丢失。

因此,当数据量较大时

function minOfArray(arr) {
  var min = Infinity
  var QUANTUM = 32768 // JavaScript 核心中已经作了硬编码 参数个数限制在65536

  for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
    var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)))
    min = Math.min(submin, min)
  }
  return min
}
var min = minOfArray([5, 6, 2, 3, 7])
// max 一样也是如此
复制代码

3、Function.prototype.bind()

JavaScript 新手常常犯的一个错误是将一个方法从对象中拿出来,而后再调用,但愿方法中的 this 是原来的对象(好比在回调中传入这个方法)。若是不作特殊处理的话,通常 this 就丢失了。

例如:

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`)
  },
  sayHi(){
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }, 1000)
  }
};

// 问题一
bottle.sayHi();
// Hello, undefined!

// 问题二
setTimeout(bottle.sayHello, 1000); 
// Hello, undefined!
复制代码

问题一的 this.nickname 是 undefined ,缘由是 this 指向是在运行函数时肯定的,而不是定义函数时候肯定的,再由于 sayHi 中 setTimeout 在全局环境下执行,因此 this 指向 setTimeout 的上下文:window。

问题二的 this.nickname 是 undefined ,是由于 setTimeout 仅仅只是获取函数 bottle.sayHello 做为 setTimeout 回调函数,this 和 bottle 对象分离了。

问题二能够写为:

// 在这种状况下,this 指向全局做用域
let func = bottle.sayHello;
setTimeout(func, 1000); 
// 用户上下文丢失
// 浏览器上,访问的其实是 Window 上下文
复制代码

那么怎么解决这两个问题喃?

解决方案一: 缓存 this 与包装

首先经过缓存 this 解决问题一 bottle.sayHi();

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`)
  },
  sayHi(){
    var _this = this // 缓存this
    setTimeout(function(){
      console.log('Hello, ', _this.nickname)
    }, 1000)
  }
};

bottle.sayHi();
// Hello, bottle
复制代码

那问题二 setTimeout(bottle.sayHello, 1000); 喃?

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

// 加一个包装层
setTimeout(() => {
  bottle.sayHello()
}, 1000); 
// Hello, bottle!
复制代码

这样看似解决了问题二,但若是咱们在 setTimeout 异步触发以前更新 bottle 值又会怎么样呢?

var bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

setTimeout(() => {
  bottle.sayHello()
}, 1000); 

// 更新 bottle
bottle = {
  nickname: "haha",
  sayHello() {
    console.log(`Hi, ${this.nickname}!`)
  }
};
// Hi, haha!
复制代码

bottle.sayHello() 最终打印为 Hi, haha! ,那么怎么解决这种事情发生喃?

解决方案二: bind

bind() 最简单的用法是建立一个新绑定函数,当这个新绑定函数被调用时,this 键值为其提供的值,其参数列表前几项值为建立时指定的参数序列,绑定函数与被调函数具备相同的函数体(ES5中)。

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  }
};

// 未绑定,“this” 指向全局做用域
let sayHello = bottle.sayHello
console.log(sayHello())
// Hello, undefined!

// 绑定
let bindSayHello = sayHello.bind(bottle)
// 建立一个新函数,将 this 绑定到 bottle 对象
console.log(bindSayHello())
// Hello, bottle!
复制代码

因此,从原来的函数和原来的对象建立一个绑定函数,则能很漂亮地解决上面两个问题:

let bottle = {
  nickname: "bottle",
  sayHello() {
    console.log(`Hello, ${this.nickname}!`);
  },
  sayHi(){
    // 使用 bind
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }.bind(this), 1000)
    
    // 或箭头函数
    setTimeout(() => {
      console.log('Hello, ', this.nickname)
    }, 1000)
  }
};

// 问题一:完美解决
bottle.sayHi()
// Hello, bottle
// Hello, bottle

let sayHello = bottle.sayHello.bind(bottle); // (*)

sayHello(); 
// Hello, bottle!

// 问题二:完美解决
setTimeout(sayHello, 1000); 
// Hello, bottle!

// 更新 bottle
bottle = {
  nickname: "haha",
  sayHello() {
    console.log(`Hi, ${this.nickname}!`)
  }
};
复制代码

问题一,能够经过 bind 或箭头函数完美解决。

最终更新 bottle 后, setTimeout(sayHello, 1000); 打印依然是 Hello, bottle!, 问题二完美解决!

1. bind 与 new

再看一个例子:

this.nickname = 'window'
let bottle = {
  nickname: 'bottle'
}
function sayHello() {
  console.log('Hello, ', this.nickname)
}

let bindBottle = sayHello.bind(bottle) // this 指向 bottle
console.log(bindBottle()) 
// Hello, bottle

console.log(new bindBottle())  // this 指向 sayHello {}
// Hello, undefined
复制代码

上面例子中,运行结果 this.nickname 输出为 undefined ,这不是全局 nickname , 也不是 bottle 对象中的 nickname ,这说明 bind 的 this 对象失效了,new 的实现中生成一个新的对象,这个时候的 this 指向的是 sayHello

注意 :绑定函数也可使用 new 运算符构造:这样作就好像已经构造了目标函数同样。提供的 this 值将被忽略,而前置参数将提供给模拟函数。

2. 二次 bind

function sayHello() {
  console.log('Hello, ', this.nickname)
}

sayHello = sayHello.bind( {nickname: "Bottle"} ).bind( {nickname: "AnGe" } );
sayHello();
// Hello, Bottle
复制代码

输出依然是 Hello, Bottle ,这是由于 func.bind(...) 返回的外来的绑定函数对象仅在建立的时候记忆上下文(若是提供了参数)。

一个函数不能做为重复绑定。

2. 偏函数

当咱们肯定一个函数的一些参数时,返回的函数(更加特定)被称为偏函数。咱们可使用 bind 来获取偏函数:

function list() {
  return Array.prototype.slice.call(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
复制代码

当咱们不想一遍又一遍重复相同的参数时,偏函数很方便。

3. 做为构造函数使用的绑定函数

function Bottle(nickname) {
  this.nickname = nickname;
}
Bottle.prototype.sayHello = function() { 
  console.log('Hello, ', this.nickname)
};

let bottle = new Bottle('bottle');
let BindBottle = Bottle.bind(null, 'bindBottle');

let b1 = new BindBottle('b1');
b1 instanceof Bottle; // true
b1 instanceof BindBottle; // true
new Bottle('bottle1') instanceof BindBottle; // true

b1.sayHello()
// Hello, bindBottle
复制代码

4、柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3
复制代码

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 以后,返回的函数就经过闭包的方式记住了 add 的第一个参数。因此说 bind 自己也是闭包的一种使用场景。

柯里化是将 f(a,b,c) 能够被以 f(a)(b)(c) 的形式被调用的转化。JavaScript 实现版本一般保留函数被正常调用和在参数数量不够的状况下返回偏函数这两个特性。

5、扩展:箭头函数

1. 没有 this

let bottle = {
  nickname: "bottle",
  sayHi(){
    setTimeout(function(){
      console.log('Hello, ', this.nickname)
    }, 1000)
    
    // 或箭头函数
    setTimeout(() => {
      console.log('Hi, ', this.nickname)
    }, 1000)
  }
};

bottle.sayHi()
// Hello, undefined
// Hi, bottle
复制代码

报错是由于 Hello, undefined 是由于运行时 this=WindowWindow.nicknameundefined

但箭头函数就没事,由于箭头函数没有 this。在外部上下文中,this 的查找与普通变量搜索彻底相同。this 指向定义时的环境。

2. 不可 new 实例化

不具备 this 天然意味着另外一个限制:箭头函数不能用做构造函数。他们不能用 new 调用。

3. 箭头函数 vs bind

箭头函数 => 和正常函数经过 .bind(this) 调用有一个微妙的区别:

  • .bind(this) 建立该函数的 “绑定版本”。
  • 箭头函数 => 不会建立任何绑定。该函数根本没有 this。在外部上下文中,this 的查找与普通变量搜索彻底相同。

4. 没有 arguments 对象

箭头函数也没有 arguments 变量。

由于咱们须要用当前的 thisarguments 转发一个调用,全部这对于装饰者来讲很是好。

例如,defer(f, ms) 获得一个函数,并返回一个包装函数,以 毫秒 为单位延迟调用:

function defer(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms)
  };
}

function sayHi(who) {
  alert('Hello, ' + who);
}

let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("John"); // 2 秒后打印 Hello, John
复制代码

没有箭头功能的状况以下所示:

function defer(f, ms) {
  return function(...args) {
    let ctx = this;
    setTimeout(function() {
      return f.apply(ctx, args);
    }, ms);
  };
}
复制代码

在这里,咱们必须建立额外的变量 argsctx,以便 setTimeout 内部的函数能够接收它们。

5. 总结

  • this 指向定义时的环境
  • 不可 new 实例化
  • this 不可变
  • 没有 arguments 对象

6、参考

装饰和转发,call/apply

7、系列文章

想看更过系列文章,点击前往 github 博客主页

8、走在最后

1. ❤️玩得开心,不断学习,并始终保持编码。👨💻

2. 若有任何问题或更独特的看法,欢迎评论或直接联系瓶子君(公众号回复 123 便可)!👀👇

3. 👇欢迎关注:前端瓶子君,每日更新!👇

前端瓶子君
相关文章
相关标签/搜索