JS
系列暂定 27 篇,从基础,到原型,到异步,到设计模式,到架构模式等。javascript
本篇是JS
系列中第 5 篇,文章主讲 JS 中 call
、 apply
、 bind
、箭头函数以及柯里化,着重介绍它们之间的区别、对比使用,深刻了解 call
、 apply
、 bind
。前端
call()
方法调用一个函数, 其具备一个指定的 this
值和多个参数(参数的列表)。java
func.call(thisArg, arg1, arg2, ...)
复制代码
它运行 func
,提供的第一个参数 thisArg
做为 this
,后面的做为参数。git
先看一个例子:github
func(1, 2, 3);
func.call(obj, 1, 2, 3)
复制代码
他们都调用的是 func
,参数是 1
,2
和 3
。设计模式
惟一的区别是 func.call
也将 this
设置为 obj
。数组
须要注意的是,设置的 thisArg 值并不必定是该函数执行时真正的 this
值,若是这个函数处于非严格模式下,则指定为 null
和 undefined
的 this
值会自动指向全局对象(浏览器中就是 window 对象),同时值为原始值(数字,字符串,布尔值)的 this
会指向该原始值的自动包装对象。浏览器
例如,在下面的代码中,咱们在对象的上下文中调用 sayWord.call(bottle)
运行 sayWord
,并 bottle
传递为 sayWord
的 this
:缓存
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
复制代码
// 非严格模式下
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
复制代码
基本思想:在子类型的构造函数内部调用父类型构造函数。闭包
注意:函数只不过是在特定环境中执行代码的对象,因此这里使用 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绑定到子类
复制代码
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]
对象在数组中的正确索引号。
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 仍然不接受类数组对象。若是传入类数组对象,它们会抛出异常。
咱们已经知道了JS 基础之: var、let、const、解构、展开、函数 一章中的扩展运算符 ...
,它能够将数组(或任何可迭代的)做为参数列表传递。所以,若是咱们将它与 call
一块儿使用,就能够实现与 apply
几乎相同的功能。
这两个调用结果几乎相同:
let args = [1, 2, 3];
func.call(context, ...args); // 使用 spread 运算符将数组做为参数列表传递
func.apply(context, args); // 与使用 call 相同
复制代码
若是咱们仔细观察,那么 call
和 apply
的使用会有一些细微的差异。
...
容许将 可迭代的 参数列表
做为列表传递给 call
。apply
只接受 类数组同样的 参数列表
。apply
最重要的用途之一是将调用传递给另外一个函数,以下所示:
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};
复制代码
wrapper
经过 anotherFunction.apply
得到了上下文 this
和 anotherFunction
的参数并返回其结果。
当外部代码调用这样的 wrapper
时,它与原始函数的调用没法区分。
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]
复制代码
Function.prototype.constructor = function (aArgs) {
var oNew = Object.create(this.prototype);
this.apply(oNew, aArgs);
return oNew;
};
复制代码
/* 找出数组中最大/小的数字 */
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 一样也是如此
复制代码
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!
, 问题二完美解决!
再看一个例子:
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 值将被忽略,而前置参数将提供给模拟函数。
function sayHello() {
console.log('Hello, ', this.nickname)
}
sayHello = sayHello.bind( {nickname: "Bottle"} ).bind( {nickname: "AnGe" } );
sayHello();
// Hello, Bottle
复制代码
输出依然是 Hello, Bottle
,这是由于 func.bind(...)
返回的外来的绑定函数对象仅在建立的时候记忆上下文(若是提供了参数)。
一个函数不能做为重复绑定。
当咱们肯定一个函数的一些参数时,返回的函数(更加特定)被称为偏函数。咱们可使用 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]
复制代码
当咱们不想一遍又一遍重复相同的参数时,偏函数很方便。
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
复制代码
在计算机科学中,柯里化(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 实现版本一般保留函数被正常调用和在参数数量不够的状况下返回偏函数这两个特性。
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=Window
, Window.nickname
为 undefined
。
但箭头函数就没事,由于箭头函数没有 this
。在外部上下文中,this
的查找与普通变量搜索彻底相同。this
指向定义时的环境。
不具备 this
天然意味着另外一个限制:箭头函数不能用做构造函数。他们不能用 new
调用。
箭头函数 =>
和正常函数经过 .bind(this)
调用有一个微妙的区别:
.bind(this)
建立该函数的 “绑定版本”。=>
不会建立任何绑定。该函数根本没有 this
。在外部上下文中,this
的查找与普通变量搜索彻底相同。箭头函数也没有 arguments
变量。
由于咱们须要用当前的 this
和 arguments
转发一个调用,全部这对于装饰者来讲很是好。
例如,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);
};
}
复制代码
在这里,咱们必须建立额外的变量 args
和 ctx
,以便 setTimeout
内部的函数能够接收它们。
想看更过系列文章,点击前往 github 博客主页
1. ❤️玩得开心,不断学习,并始终保持编码。👨💻
2. 若有任何问题或更独特的看法,欢迎评论或直接联系瓶子君(公众号回复 123 便可)!👀👇
3. 👇欢迎关注:前端瓶子君,每日更新!👇