Tasking:
- apply、call、bind 这三个方法之间有什么区别?
- 怎样实现一个 apply 或者 call 的方法?
方法的基本介绍
new MDN文档
原理介绍
new 关键词的主要作用就是执行一个构造函数、返回一个实例对象,在 new 的过程中,根据构造函数的情况,来确定是否可以接受参数的传递。 语法:
new constructor[([arguments])]
参数: constructor 一个指定对象实例的类型的类或函数 arguments 一个用于被 constructor 调用的参数列表 tips:> > 关于对象的 > constructor> ,参见 > Object.prototype.constructor ```javascript function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex; }
const rand = new Person(“Rand McNally”, 33, “M”); const ken = new Person(“Ken Jones”, 39, “M”);
<a name="kuMuj"></a>
##### new 关键字会进行如下操作:
- 创建一个空的对象(即{})
- 链接该对象(设置该对象的constructor)到另一个对象
- 将新创建的对象作为this的上下文
- 如果该函数没有返回对象,则返回this
<a name="ujuDR"></a>
##### 创建一个自定义对象所需执行的流程:
- 通过编写函数定义对象类型
- 通过new来创建对象实例
创建一个对象类型,需要创建一个指定其名称和属性的函数;对象的属性可以指向其他对象
<a name="yZqs4"></a>
##### 当代码new Person(...)执行时,会发生以下事情:
- 一个继承自Person.prototype的新对象被创建
- 使用指定的参数调用构造函数Person,并将this绑定到新创建的对象;new Person等同于new Person(),也就是没有指定参数列表,Person不带任何参数调用的情况
- 有构造函数返回的对象就是new表达式的结果;如果构造函数没有显示返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是开发者可以选择主动返回对象,来覆盖正常的对象创建步骤)
> **如果你没有使用 **`new` **运算符,****构造函数会像其他的常规函数一样被调用,**** 并**_不会创建一个对象**。**_**在这种情况****下,**`this`**的指向也是不一样的。**
<a name="L3C9o"></a>
#### call [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call)
<a name="i8NGn"></a>
##### 原理介绍
> `call() `方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
> 语法:`function.call(thisArg, arg1, arg2, arg3, ....)`
> 参数:
> thisArg
> 可选的。在 _`function`_ 函数运行时使用的 `this` 值。请注意,`this`可能不是该方法看到的实际值:如果这个函数处于[非严格模式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)下,则指定为 `null` 或 `undefined` 时会自动替换为指向全局对象,原始值会被包装。
> arg1, arg2, arg3,...
> 指定的参数列表;如果函数不需要传参则可以不用传参
```javascript
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
console.log(new Food('cheese', 5).name); // cheese
call 返回值
使用调用者提供的 this
值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined
。
描述
call()
允许为不同的对象分配和调用属于一个对象的函数/方法call()
提供新的this值给当前调用的函数/方法。可使用call()来实现继承:写一个方法,然后让另一个新的对象来继承它(而不是在新对象中再写一次这个方法)
实践
- 使用call方法调用腹肌构造函数(函数) ```javascript function Product(name, price) { this.name = name; this.price = price; }
function Food(name, price) { Product.call(this, name, price); this.category = ‘food’; }
function Toy(name, price) { Product.call(this, name, price); this.category = ‘toy’; }
const cheese = new Food(‘feta’, 5); const fun = new Toy(‘robot’, 40);
console.log(cheese); // Food {name: “feta”, price: 5, category: “food”} console.log(fun); // Toy {name: “robot”, price: 40, category: “toy”}
> 实例化了两个构造函数都分别实现了继承父级构造函数的属性(也就是在当前作用于内改变this的指向);使用Food和Toy构造函数创建的对象实例都会拥有在product构造函数中添加name属性和price属性,但category属性是在各自的构造函数中定义的;
- 使用call方法调用匿名函数
```javascript
let animals = [
{ species: 'Lion', name: 'King' },
{ species: 'Whale', name: 'Fail' }
];
for (let i = 0; i < animals.length; i++) {
((i) => {
this.print = () => {
console.log(`#${i} ${this.species}:${this.name}`)
}
this.print();
}).call(animals[i], i);
}
console.log(animals)
以上打印结果下图所示:
通过以上代码可以看出,在
for
循环体内,创建了一个匿名函数,然后通过该函数的call
方法,将每个数组元素作为指定的this
值执行了那个匿名函数。这个匿名函数的主要目的是给每个数组元素对象添加一个this
值传入那个匿名函数(普通参数就可以)。
使用call方法调用函数并且指定上下文的
this
const obj = {
animal: 'cats', sleepDuration: '12 and 16 hours'
};
function greet() {
const reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
console.log(reply); // cats typically sleep between 12 and 16 hours
}
greet.call(obj);
当调用greet() 方法的时候,该方法的this值会绑定obj对象,从而改变了this的指向
使用call方法调用函数并且不指定第一个参数 ```javascript const sData = ‘Wisen’; function display() { console.log(‘sData value is %s ‘, this.sData); }
display.call(); // sData value is undefined
> 调用display方法,但并没有传递它的第一个参数;如果没有传递第一个参数,this的值将会绑定全局对象;
> 在严格模式下,this的值将会是undefined。
<a name="A5Ce2"></a>
#### apply [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)
<a name="8qcfl"></a>
##### 原理介绍:
> apply()方法调用一个具有给定this值得函数,以及以一个数组的形式提供的参数
> 语法:`function.apply(thisArg, [argsArray])`
> 参数:
> thisArg
> 必选项,在函数运行时使用的this值。
> this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null和undefined时会 自动替换为指向全局对象,原始值会被包装。
> argsArray
> 可选参数,一个数组和类数组对象,其中的数组元素将作为单独的参数传给function函数;如果该函数的值为null或undefined,则表示不需要传入任何参数。从ECMAScript开始可以使用类数组对象。
<a name="EhJUw"></a>
##### 返回值
调用有指定this值和参数的函数的结果
<a name="hsyRv"></a>
##### 描述
在调用一个存在的函数时,可以为其制定一个this对象;this指当前对象,也就是正在调用这个函数的对象;使用apply可以只写一次这个方法然后再另一个对象中继承它,而不用在新对象中重复写该方法。arguments对象作为argsArray参数;arguments是一个函数的局部变量;它可以被用作调用对象的所有未指定的参数;这样就可以在使用apply函数的时候就不需要知道被调用对象的所有参数,而是用arguments来吧所有的参数传递给被调用对象;被调用对象接下来就负责处理这些参数。<br />从es5开始,可以使用任何类型的类数组对象,也就是说只要有一个length属性`(0..length-1)`范围的整数属性;Chrome14和IE9及其以下版本任然不接收类数组对象,若果传入类数组对象,她们会抛出异常。
<a name="PkE3k"></a>
##### 实践:
- 用apply将数组各项添加到另一个数组
```javascript
let array = ['a', 'b'];
let elements = [0, 1, 2];
// 使用apply方法
array.push.apply(array, elements);
// 不使用apply的方法
// array.push(...elements);
// array = array.concat(elements);
console.info(array); // ["a", "b", 0, 1, 2]
- 使用apply和内置函数
比如:我们将用Math.max/Math.min求得数组中的最大小值
/* 找出数组中最大/小的数字 */
let numbers = [5, 6, 2, 3, 7];
/* 使用Math.min/Math.max以及apply 函数时的代码 */
let max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
let min = Math.min.apply(null, numbers);
/* 对比:简单循环算法 */
max = -Infinity;
min = +Infinity;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] > max) max = numbers[i];
if (numbers[i] < min) min = numbers[i];
}
console.log(max, min)
tips: 如果按照上面方式调用apply,有超出JavaScript引擎参数长度上线的风险;一个方法传入过多参数时的后果在不同JavaScript引擎中表现不同;(JavaScriptCore引擎中有被硬编码的参数个数上限:65536);这是因为此(实际上也是任何用到超大栈空间的行为的自然表现)限制是不明确的,一些引擎会抛出异常,更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。比如:假设某个引擎的方法参数上线为4(实际上限当然要高得多),上面的代码执行后,真正被传递到apply的参数为
5, 6, 2, 3
,而不是完整的数组。
如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法
function minOfArray(arr) {
let min = Infinity;
let QUANTUM = 32768;
// for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
// let submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
// min = Math.min(submin, min);
// }
for (const key in arr) {
let submin = Math.min.apply(null, arr.slice(key, Math.min(key + QUANTUM, arr.length)));
min = Math.min(submin, min);
}
return min;
}
let min = minOfArray([5, 6, 2, 3, 7]);
console.log('min:', min); // min: 2
使用apply来链接构造器
创建一个全局
Function
对象的construct方法 ,来使你能够在构造器中使用一个类数组对象而非参数列表。
Function.prototype.construct = function (aArgs) {
var oNew = Object.create(this.prototype);
this.apply(oNew, aArgs);
return oNew;
};
注意: 上面使用的Object.create()
方法相对来说比较新。另一种可选的方法,请考虑如下替代方法:
Using Object.__proto__
:
Function.prototype.construct = function (aArgs) {
var oNew = {};
oNew.__proto__ = this.prototype;
this.apply(oNew, aArgs);
return oNew;
};
使用闭包:
Function.prototype.construct = function(aArgs) {
var fConstructor = this, fNewConstr = function() {
fConstructor.apply(this, aArgs);
};
fNewConstr.prototype = fConstructor.prototype;
return new fNewConstr();
};
使用 Function 构造器:
Function.prototype.construct = function (aArgs) {
var fNewConstr = new Function("");
fNewConstr.prototype = this.prototype;
var oNew = new fNewConstr();
this.apply(oNew, aArgs);
return oNew;
};
使用示例:
function MyConstructor (arguments) {
for (var nProp = 0; nProp < arguments.length; nProp++) {
this["property" + nProp] = arguments[nProp];
}
}
var myArray = [4, "Hello world!", false];
var myInstance = new MyConstructor(myArray); //Fix MyConstructor.construct is not a function
console.log(myInstance.property1); // logs "Hello world!"
console.log(myInstance instanceof MyConstructor); // logs "true"
console.log(myInstance.constructor); // logs "MyConstructor"
bind
原理介绍
**bind()**
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 语法:function.bind(thisArg[, arg1[,...]])
参数: thisArg: 调用绑定函数作为this
参数传递给目标函数的值。如果使用new
运算符构造绑定函数,则忽略改制。当使用bind
在setTimeout
中创建一个函数(作为对调提供)时,作为thisArg
传递的任何原始值都将转换为object
。吐过bind
函数的参数列表为空,或者thisArg
是null
或undefined
,执行作用域的this
将被视为新函数的thisArg
arg1, arg2, … 当目标函数被调用时,被预置入绑定函数的参数列表中的参数
返回值
返回一个原函数的拷贝,并拥有指定
this
值和初始参数
描述
bind()函数会创建一个新的绑定函数(bound function, BF)。绑定函数是一个exotic function object(怪异函数对象,ECMAScript2015中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。 绑定函数具有以下内部属性:
- [[BoundTargetFunction]]—包装的函数对象
- [[BoundThis]]—在调用包装函数时始终作为this值传递的值
- [[BoundArguments]]—列表,在对包装函数做任何调用都会优先用列表元素填充参数列表
- [[call]]—执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表
当调用绑定函数时, 它调用[[BoundTargetFunction]]上的内部方法[[call]],就像这样call(boundThis, args)。其中boundThis是[[boundTHis]], args是[[BoundArguments]]加上通过函数调用传入的参数列表
绑定函数也可以使用new
运算符构造,他会表现为目标函数已经构建完毕似的,提供this值会被忽略,但前置参数仍会提供给模拟函数。
实践
- 创建绑定函数
bind()最简单的用法是创建一个函数,不论是怎么调用,这个函数都有同样的this值。JavaScript新手经常犯的一个错误就是将一个方法从对象中拿出来,然后再调用,期望方法中的this是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象;基于这个函数,用原始的对象创建一个绑定函数,巧妙的解决这个问题:
this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象
let module = {
x: 81,
getX: function () { return this.x; }
};
module.getX(); // 81
let retrieveX = module.getX;
retrieveX(); // 返回 9 - 因为函数是在全局作用域中调用的
// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
let boundGetX = retrieveX.bind(module);
boundGetX(); // 81
- 偏函数
bind()的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递绑定函数的参数会跟他们后面。
function list() {
return Array.prototype.slice.call(arguments);
}
function addArguments(arg1, arg2) {
return arg1 + arg2
}
let list1 = list(1, 2, 3); // [1, 2, 3]
let result1 = addArguments(1, 2); // 3
// 创建一个函数,它拥有预设参数列表。
let leadingThirtysevenList = list.bind(null, 37);
// 创建一个函数,它拥有预设的第一个参数
let addThirtySeven = addArguments.bind(null, 37);
let list2 = leadingThirtysevenList(); // [37]
let list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
let result2 = addThirtySeven(5); // 37 + 5 = 42
let result3 = addThirtySeven(5, 10); // 37 + 5 = 42 ,第二个参数被忽略
- 配合setTimeout
在默认情况下,使用window.setTimeout()时, this关键字会指向window(或global)对象。当类的方法中需要this指向类的实例时,可能需要显示的把this
绑定到回调函数,就不会丢失该实例的引用
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function () {
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function () {
console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};
let flower = new LateBloomer();
flower.bloom(); // 一秒钟后, 调用 'declare' 方法
- 作为构造函数使用的绑定函数
绑定函数自动适应与使用new操作符去构造一个有目标函数创建的新实例;当一个绑定函数是用来构建一个值得,原来提供的this就会被忽略;不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return this.x + ',' + this.y;
};
let p = new Point(1, 2);
p.toString(); // '1,2'
let emptyObj = {};
let YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 本页下方的 polyfill 不支持运行这行代码,
// 但使用原生的 bind 方法运行是没问题的:
let YAxisPoint = Point.bind(null, 0/*x*/);
/*(译注:polyfill 的 bind 方法中,如果把 bind 的第一个参数加上,
即对新绑定的 this 执行 Object(this),包装为对象,
因为 Object(null) 是 {},所以也可以支持)*/
let axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new YAxisPoint(17, 42) instanceof Point; // true
不需要做特别的处理就可以new操作符创建一个绑定函数。也就是说,不需要特别处理就可以创建一个可以被直接调用的绑定函数,即使希望绑定函数使用new操作符来调用
// ...接着上面的代码继续的话,
// 这个例子可以直接在你的 JavaScript 控制台运行
// 仍然能作为一个普通函数来调用
// (即使通常来说这个不是被期望发生的)
YAxisPoint(13);
emptyObj.x + ',' + emptyObj.y; // '0,13'
如果希望一个绑定函数要么只能用new操作符,要么只能直接调用,要么就必须在目标函数上显示规定这个限制
- 快捷调用
当需要一个特定的this值得函数创建一个接近的时候,bind()也很好用;可以使用Array.prototype.slice来讲一个类似于数组的对象转换成一个真正的数组
let slice = Array.prototype.slice;
slice.apply(arguments);
用bind()可以是这个过程变得简单
let unboundSlice = Array.prototype.slice;
let slice = Function.prototype.apply.bind(unboundSlice);
slice([1, 2, 3, 4]);
如何实现它们呢?
这个问题也时很多公司面试的高频题目,下面来动手实践实践!
new 的实现
根据以上介绍,我们便可清晰的知道new的执行过程,那么实现则可现列出一个Tasking
Tasking
- 让实例可以访问到私有属性
- 让实例可以访问构造函数原型()所在原型链上的属性(proto)
构造函数返回的最后结果是引用类型 ```javascript function _new(ctor, …args) { if (typeof ctor !== ‘function’) {
throw 'ctor must be a function';
} let obj = new Object(); obj.proto = Object.create(ctor.prototype); let res = ctor.apply(obj, […args]);
let isObject = typeof res === ‘object’ && typeof res !== null; let isFunction = typeof res === ‘function’; return isObject || isFunction ? res : obj; };
function calc() { console.log(‘calc’) }
_new(calc); // calc
具体图如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/541953/1612689845867-fc775cb8-4c42-4fa1-adbd-512723155474.png#align=left&display=inline&height=237&margin=%5Bobject%20Object%5D&name=image.png&originHeight=237&originWidth=325&size=21015&status=done&style=none&width=325)
<a name="yB7Wf"></a>
#### call的实现
```javascript
// 方法一
Function.prototype.ForestCall = function (context, ...args) {
context = context || window;
context.fn = this;
let result = eval('context.fn(...args)');
delete context.fn;
return result;
};
// 方法二
Function.prototype.myCall = function (context) {
// 判断context是否存在,不存在设置为window
context = context ? Object(context) : window;
// 处理参数
const args = [...arguments].slice(1);
// 要将this指向改为context,需要用context来调用
context.fn = this; // 这里的this是原函数
const result = context.fn(...args); // 执行原函数,此时因为是context调用,因此函数中的this指向了context
delete context.fn;
return result;
}
const obj = {
name: 'banana',
category: 'fruit'
}
function getCategory() {
console.log(this.category)
}
getCategory.ForestCall(obj)
getCategory.myCall(obj)
**eval()**
函数会将传入的字符串当做 JavaScript 代码进行执行。eval() MDN 语法:eval(string)
参数: string:一个表示JavaScript表达式、语句或一系列语句的字符串;表达式可包含变量与已存在对象的属性 返回值: 返回字符串中代码的返回值,如果返回值为空,则返回undefined 描述:
eval()
的参数是一个字符串。如果字符串表示的是表达式,eval()会对表达式进行求值;如果参数表示一个或者多个JavaScript语句;那么eval()就会执行这些语句;不需要用eval()来执行一个算术表达式:因为JavaScript可以自动为算术表达式求值;- 如果你以字符串的形式构造了算术表达式,那么可以在后面用
eval()
对它求值。- 如果
eval()
的参数不是字符串,eval()
会将参数原封不动地返回。
apply的实现
let array = ['a', 'b'];
let elements = [0, 1, 2];
// 使用apply方法
array.push.ForestApply(array, elements);
console.log(array); // ["a", "b", 0, 1, 2]