浅析new、call、apply、bind的底层逻辑 - 图1

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”);

  1. <a name="kuMuj"></a>
  2. ##### new 关键字会进行如下操作:
  3. - 创建一个空的对象(即{})
  4. - 链接该对象(设置该对象的constructor)到另一个对象
  5. - 将新创建的对象作为this的上下文
  6. - 如果该函数没有返回对象,则返回this
  7. <a name="ujuDR"></a>
  8. ##### 创建一个自定义对象所需执行的流程:
  9. - 通过编写函数定义对象类型
  10. - 通过new来创建对象实例
  11. 创建一个对象类型,需要创建一个指定其名称和属性的函数;对象的属性可以指向其他对象
  12. <a name="yZqs4"></a>
  13. ##### 当代码new Person(...)执行时,会发生以下事情:
  14. - 一个继承自Person.prototype的新对象被创建
  15. - 使用指定的参数调用构造函数Person,并将this绑定到新创建的对象;new Person等同于new Person(),也就是没有指定参数列表,Person不带任何参数调用的情况
  16. - 有构造函数返回的对象就是new表达式的结果;如果构造函数没有显示返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是开发者可以选择主动返回对象,来覆盖正常的对象创建步骤)
  17. > **如果你没有使用 **`new` **运算符,****构造函数会像其他的常规函数一样被调用,**** 并**_不会创建一个对象**。**_**在这种情况****下,**`this`**的指向也是不一样的。**
  18. <a name="L3C9o"></a>
  19. #### call [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call)
  20. <a name="i8NGn"></a>
  21. ##### 原理介绍
  22. > `call() `方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
  23. > 语法:`function.call(thisArg, arg1, arg2, arg3, ....)`
  24. > 参数:
  25. > thisArg
  26. > 可选的。在 _`function`_ 函数运行时使用的 `this` 值。请注意,`this`可能不是该方法看到的实际值:如果这个函数处于[非严格模式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)下,则指定为 `null` 或 `undefined` 时会自动替换为指向全局对象,原始值会被包装。
  27. > arg1, arg2, arg3,...
  28. > 指定的参数列表;如果函数不需要传参则可以不用传参
  29. ```javascript
  30. function Product(name, price) {
  31. this.name = name;
  32. this.price = price;
  33. }
  34. function Food(name, price) {
  35. Product.call(this, name, price);
  36. this.category = 'food';
  37. }
  38. 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”}

  1. > 实例化了两个构造函数都分别实现了继承父级构造函数的属性(也就是在当前作用于内改变this的指向);使用FoodToy构造函数创建的对象实例都会拥有在product构造函数中添加name属性和price属性,但category属性是在各自的构造函数中定义的;
  2. - 使用call方法调用匿名函数
  3. ```javascript
  4. let animals = [
  5. { species: 'Lion', name: 'King' },
  6. { species: 'Whale', name: 'Fail' }
  7. ];
  8. for (let i = 0; i < animals.length; i++) {
  9. ((i) => {
  10. this.print = () => {
  11. console.log(`#${i} ${this.species}:${this.name}`)
  12. }
  13. this.print();
  14. }).call(animals[i], i);
  15. }
  16. console.log(animals)

以上打印结果下图所示:

image.png

通过以上代码可以看出,在for循环体内,创建了一个匿名函数,然后通过该函数的call方法,将每个数组元素作为指定的this值执行了那个匿名函数。这个匿名函数的主要目的是给每个数组元素对象添加一个print方法,这个print方法可以打印出各元素在数组中的正确索引号。当然,这里不是必须得让数组元素作为this值传入那个匿名函数(普通参数就可以)。

  • 使用call方法调用函数并且指定上下文的this

    1. const obj = {
    2. animal: 'cats', sleepDuration: '12 and 16 hours'
    3. };
    4. function greet() {
    5. const reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
    6. console.log(reply); // cats typically sleep between 12 and 16 hours
    7. }
    8. 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

  1. > 调用display方法,但并没有传递它的第一个参数;如果没有传递第一个参数,this的值将会绑定全局对象;
  2. > 在严格模式下,this的值将会是undefined
  3. <a name="A5Ce2"></a>
  4. #### apply [MDN文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)
  5. <a name="8qcfl"></a>
  6. ##### 原理介绍:
  7. > apply()方法调用一个具有给定this值得函数,以及以一个数组的形式提供的参数
  8. > 语法:`function.apply(thisArg, [argsArray])`
  9. > 参数:
  10. > thisArg
  11. > 必选项,在函数运行时使用的this值。
  12. > this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为nullundefined时会 自动替换为指向全局对象,原始值会被包装。
  13. > argsArray
  14. > 可选参数,一个数组和类数组对象,其中的数组元素将作为单独的参数传给function函数;如果该函数的值为nullundefined,则表示不需要传入任何参数。从ECMAScript开始可以使用类数组对象。
  15. <a name="EhJUw"></a>
  16. ##### 返回值
  17. 调用有指定this值和参数的函数的结果
  18. <a name="hsyRv"></a>
  19. ##### 描述
  20. 在调用一个存在的函数时,可以为其制定一个this对象;this指当前对象,也就是正在调用这个函数的对象;使用apply可以只写一次这个方法然后再另一个对象中继承它,而不用在新对象中重复写该方法。arguments对象作为argsArray参数;arguments是一个函数的局部变量;它可以被用作调用对象的所有未指定的参数;这样就可以在使用apply函数的时候就不需要知道被调用对象的所有参数,而是用arguments来吧所有的参数传递给被调用对象;被调用对象接下来就负责处理这些参数。<br />从es5开始,可以使用任何类型的类数组对象,也就是说只要有一个length属性`(0..length-1)`范围的整数属性;Chrome14IE9及其以下版本任然不接收类数组对象,若果传入类数组对象,她们会抛出异常。
  21. <a name="PkE3k"></a>
  22. ##### 实践:
  23. - apply将数组各项添加到另一个数组
  24. ```javascript
  25. let array = ['a', 'b'];
  26. let elements = [0, 1, 2];
  27. // 使用apply方法
  28. array.push.apply(array, elements);
  29. // 不使用apply的方法
  30. // array.push(...elements);
  31. // array = array.concat(elements);
  32. console.info(array); // ["a", "b", 0, 1, 2]
  • 使用apply和内置函数

比如:我们将用Math.max/Math.min求得数组中的最大小值

  1. /* 找出数组中最大/小的数字 */
  2. let numbers = [5, 6, 2, 3, 7];
  3. /* 使用Math.min/Math.max以及apply 函数时的代码 */
  4. let max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
  5. let min = Math.min.apply(null, numbers);
  6. /* 对比:简单循环算法 */
  7. max = -Infinity;
  8. min = +Infinity;
  9. for (let i = 0; i < numbers.length; i++) {
  10. if (numbers[i] > max) max = numbers[i];
  11. if (numbers[i] < min) min = numbers[i];
  12. }
  13. console.log(max, min)

tips: 如果按照上面方式调用apply,有超出JavaScript引擎参数长度上线的风险;一个方法传入过多参数时的后果在不同JavaScript引擎中表现不同;(JavaScriptCore引擎中有被硬编码的参数个数上限:65536);这是因为此(实际上也是任何用到超大栈空间的行为的自然表现)限制是不明确的,一些引擎会抛出异常,更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。比如:假设某个引擎的方法参数上线为4(实际上限当然要高得多),上面的代码执行后,真正被传递到apply的参数为5, 6, 2, 3,而不是完整的数组。

如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法

  1. function minOfArray(arr) {
  2. let min = Infinity;
  3. let QUANTUM = 32768;
  4. // for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
  5. // let submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
  6. // min = Math.min(submin, min);
  7. // }
  8. for (const key in arr) {
  9. let submin = Math.min.apply(null, arr.slice(key, Math.min(key + QUANTUM, arr.length)));
  10. min = Math.min(submin, min);
  11. }
  12. return min;
  13. }
  14. let min = minOfArray([5, 6, 2, 3, 7]);
  15. console.log('min:', min); // min: 2

使用apply来链接构造器

创建一个全局Function 对象的construct方法 ,来使你能够在构造器中使用一个类数组对象而非参数列表。

  1. Function.prototype.construct = function (aArgs) {
  2. var oNew = Object.create(this.prototype);
  3. this.apply(oNew, aArgs);
  4. return oNew;
  5. };

注意: 上面使用的Object.create()方法相对来说比较新。另一种可选的方法,请考虑如下替代方法:
Using Object.__proto__:

  1. Function.prototype.construct = function (aArgs) {
  2. var oNew = {};
  3. oNew.__proto__ = this.prototype;
  4. this.apply(oNew, aArgs);
  5. return oNew;
  6. };

使用闭包:

  1. Function.prototype.construct = function(aArgs) {
  2. var fConstructor = this, fNewConstr = function() {
  3. fConstructor.apply(this, aArgs);
  4. };
  5. fNewConstr.prototype = fConstructor.prototype;
  6. return new fNewConstr();
  7. };

使用 Function 构造器:

  1. Function.prototype.construct = function (aArgs) {
  2. var fNewConstr = new Function("");
  3. fNewConstr.prototype = this.prototype;
  4. var oNew = new fNewConstr();
  5. this.apply(oNew, aArgs);
  6. return oNew;
  7. };

使用示例:

  1. function MyConstructor (arguments) {
  2. for (var nProp = 0; nProp < arguments.length; nProp++) {
  3. this["property" + nProp] = arguments[nProp];
  4. }
  5. }
  6. var myArray = [4, "Hello world!", false];
  7. var myInstance = new MyConstructor(myArray); //Fix MyConstructor.construct is not a function
  8. console.log(myInstance.property1); // logs "Hello world!"
  9. console.log(myInstance instanceof MyConstructor); // logs "true"
  10. console.log(myInstance.constructor); // logs "MyConstructor"

bind

原理介绍

**bind()** 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 语法:function.bind(thisArg[, arg1[,...]]) 参数: thisArg: 调用绑定函数作为this参数传递给目标函数的值。如果使用new 运算符构造绑定函数,则忽略改制。当使用bindsetTimeout中创建一个函数(作为对调提供)时,作为thisArg 传递的任何原始值都将转换为object。吐过bind 函数的参数列表为空,或者thisArgnullundefined,执行作用域的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是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象;基于这个函数,用原始的对象创建一个绑定函数,巧妙的解决这个问题:

  1. this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象
  2. let module = {
  3. x: 81,
  4. getX: function () { return this.x; }
  5. };
  6. module.getX(); // 81
  7. let retrieveX = module.getX;
  8. retrieveX(); // 返回 9 - 因为函数是在全局作用域中调用的
  9. // 创建一个新函数,把 'this' 绑定到 module 对象
  10. // 新手可能会将全局变量 x 与 module 的属性 x 混淆
  11. let boundGetX = retrieveX.bind(module);
  12. boundGetX(); // 81
  • 偏函数

    bind()的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递绑定函数的参数会跟他们后面。

  1. function list() {
  2. return Array.prototype.slice.call(arguments);
  3. }
  4. function addArguments(arg1, arg2) {
  5. return arg1 + arg2
  6. }
  7. let list1 = list(1, 2, 3); // [1, 2, 3]
  8. let result1 = addArguments(1, 2); // 3
  9. // 创建一个函数,它拥有预设参数列表。
  10. let leadingThirtysevenList = list.bind(null, 37);
  11. // 创建一个函数,它拥有预设的第一个参数
  12. let addThirtySeven = addArguments.bind(null, 37);
  13. let list2 = leadingThirtysevenList(); // [37]
  14. let list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
  15. let result2 = addThirtySeven(5); // 37 + 5 = 42
  16. let result3 = addThirtySeven(5, 10); // 37 + 5 = 42 ,第二个参数被忽略
  • 配合setTimeout

在默认情况下,使用window.setTimeout()时, this关键字会指向window(或global)对象。当类的方法中需要this指向类的实例时,可能需要显示的把this 绑定到回调函数,就不会丢失该实例的引用

  1. function LateBloomer() {
  2. this.petalCount = Math.ceil(Math.random() * 12) + 1;
  3. }
  4. // 在 1 秒钟后声明 bloom
  5. LateBloomer.prototype.bloom = function () {
  6. window.setTimeout(this.declare.bind(this), 1000);
  7. };
  8. LateBloomer.prototype.declare = function () {
  9. console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
  10. };
  11. let flower = new LateBloomer();
  12. flower.bloom(); // 一秒钟后, 调用 'declare' 方法
  • 作为构造函数使用的绑定函数

绑定函数自动适应与使用new操作符去构造一个有目标函数创建的新实例;当一个绑定函数是用来构建一个值得,原来提供的this就会被忽略;不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.toString = function () {
  6. return this.x + ',' + this.y;
  7. };
  8. let p = new Point(1, 2);
  9. p.toString(); // '1,2'
  10. let emptyObj = {};
  11. let YAxisPoint = Point.bind(emptyObj, 0/*x*/);
  12. // 本页下方的 polyfill 不支持运行这行代码,
  13. // 但使用原生的 bind 方法运行是没问题的:
  14. let YAxisPoint = Point.bind(null, 0/*x*/);
  15. /*(译注:polyfill 的 bind 方法中,如果把 bind 的第一个参数加上,
  16. 即对新绑定的 this 执行 Object(this),包装为对象,
  17. 因为 Object(null) 是 {},所以也可以支持)*/
  18. let axisPoint = new YAxisPoint(5);
  19. axisPoint.toString(); // '0,5'
  20. axisPoint instanceof Point; // true
  21. axisPoint instanceof YAxisPoint; // true
  22. new YAxisPoint(17, 42) instanceof Point; // true

不需要做特别的处理就可以new操作符创建一个绑定函数。也就是说,不需要特别处理就可以创建一个可以被直接调用的绑定函数,即使希望绑定函数使用new操作符来调用

  1. // ...接着上面的代码继续的话,
  2. // 这个例子可以直接在你的 JavaScript 控制台运行
  3. // 仍然能作为一个普通函数来调用
  4. // (即使通常来说这个不是被期望发生的)
  5. YAxisPoint(13);
  6. emptyObj.x + ',' + emptyObj.y; // '0,13'

如果希望一个绑定函数要么只能用new操作符,要么只能直接调用,要么就必须在目标函数上显示规定这个限制

  • 快捷调用

当需要一个特定的this值得函数创建一个接近的时候,bind()也很好用;可以使用Array.prototype.slice来讲一个类似于数组的对象转换成一个真正的数组

  1. let slice = Array.prototype.slice;
  2. slice.apply(arguments);

用bind()可以是这个过程变得简单

  1. let unboundSlice = Array.prototype.slice;
  2. let slice = Function.prototype.apply.bind(unboundSlice);
  3. slice([1, 2, 3, 4]);

如何实现它们呢?

这个问题也时很多公司面试的高频题目,下面来动手实践实践!

new 的实现

根据以上介绍,我们便可清晰的知道new的执行过程,那么实现则可现列出一个Tasking

Tasking
  • 让实例可以访问到私有属性
  • 让实例可以访问构造函数原型()所在原型链上的属性(proto
  • 构造函数返回的最后结果是引用类型 ```javascript function _new(ctor, …args) { if (typeof ctor !== ‘function’) {

    1. 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

  1. 具体图如下:<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)
  2. <a name="yB7Wf"></a>
  3. #### call的实现
  4. ```javascript
  5. // 方法一
  6. Function.prototype.ForestCall = function (context, ...args) {
  7. context = context || window;
  8. context.fn = this;
  9. let result = eval('context.fn(...args)');
  10. delete context.fn;
  11. return result;
  12. };
  13. // 方法二
  14. Function.prototype.myCall = function (context) {
  15. // 判断context是否存在,不存在设置为window
  16. context = context ? Object(context) : window;
  17. // 处理参数
  18. const args = [...arguments].slice(1);
  19. // 要将this指向改为context,需要用context来调用
  20. context.fn = this; // 这里的this是原函数
  21. const result = context.fn(...args); // 执行原函数,此时因为是context调用,因此函数中的this指向了context
  22. delete context.fn;
  23. return result;
  24. }
  25. const obj = {
  26. name: 'banana',
  27. category: 'fruit'
  28. }
  29. function getCategory() {
  30. console.log(this.category)
  31. }
  32. getCategory.ForestCall(obj)
  33. getCategory.myCall(obj)

**eval()** 函数会将传入的字符串当做 JavaScript 代码进行执行。eval() MDN 语法: eval(string) 参数: string:一个表示JavaScript表达式、语句或一系列语句的字符串;表达式可包含变量与已存在对象的属性 返回值: 返回字符串中代码的返回值,如果返回值为空,则返回undefined 描述:

  • eval() 的参数是一个字符串。如果字符串表示的是表达式,eval()会对表达式进行求值;如果参数表示一个或者多个JavaScript语句;那么eval()就会执行这些语句;不需要用eval()来执行一个算术表达式:因为JavaScript可以自动为算术表达式求值;
  • 如果你以字符串的形式构造了算术表达式,那么可以在后面用 eval()对它求值。
  • 如果eval() 的参数不是字符串, eval() 会将参数原封不动地返回。


apply的实现

  1. let array = ['a', 'b'];
  2. let elements = [0, 1, 2];
  3. // 使用apply方法
  4. array.push.ForestApply(array, elements);
  5. console.log(array); // ["a", "b", 0, 1, 2]