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,...> 指定的参数列表;如果函数不需要传参则可以不用传参```javascriptfunction 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方法调用匿名函数```javascriptlet 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方法调用函数并且指定上下文的
thisconst 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将数组各项添加到另一个数组```javascriptlet 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 functionconsole.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将被视为新函数的thisArgarg1, 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(); // 81let 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 = 42let 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 秒钟后声明 bloomLateBloomer.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; // trueaxisPoint instanceof YAxisPoint; // truenew 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 /><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是否存在,不存在设置为windowcontext = context ? Object(context) : window;// 处理参数const args = [...arguments].slice(1);// 要将this指向改为context,需要用context来调用context.fn = this; // 这里的this是原函数const result = context.fn(...args); // 执行原函数,此时因为是context调用,因此函数中的this指向了contextdelete 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]
