Интересный JS-код.

JavaScript
Интересный JS-код.

Сложность: умеренная

Мы часто видим строку кода, похожую на следующую, в исходном коде уровня фреймворка, например:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

В этом предложении кода используются как метод вызова, так и метод связывания.На первый взгляд, это немного головокружительно! Что это пытается сделать?

Не беда, давайте вызовем его и попробуем передать в разных типах, эффект будет такой:

console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

Из результатов видно, что основной функцией этого метода является определение типа объекта. Но обычно при обнаружении типов мы можем увидеть следующие реализации кода:

var toStr2 = obj => Object.prototype.toString.call(obj);

console.log(toStr2({}));      // "[object Object]"
console.log(toStr2([]));      // "[object Array]"
console.log(toStr2(123));     // "[object Number]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2("abc"));   // "[object String]"
console.log(toStr2(new Date));// "[object Date]"

Студенты, знакомые с bind и call, должны знать, что эти два метода по сути одинаковы, а второй метод более лаконичен.Для получения нужных функций можно использовать только один вызов, а логика кода понятна и понятна. , Почему первый больше используется во многих фреймворках?

На самом деле, основная причина防止原型污染, например, затираем в бизнес-кодеObject.prototype.toStringметод, второй способ записи не даст правильного результата, в то время как первый способ записи все еще будет работать. Давайте попробуем это с кодом:

var toStr1 = Function.prototype.call.bind(Object.prototype.toString);

var toStr2 = obj => Object.prototype.toString.call(obj);

Object.prototype.toString = function(){
  return'toString方法被覆盖!';
}
// 接着我们再调用上述方法

// toStr1调用结果如下:
console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

// toStr2调用结果如下:
console.log(toStr2({}));      // "toString方法被覆盖!"
console.log(toStr2([]));      // "toString方法被覆盖!"
console.log(toStr2(123));     // "toString方法被覆盖!"
console.log(toStr2("abc"));   // "toString方法被覆盖!"
console.log(toStr2("abc"));   // "toString方法被覆盖!"
console.log(toStr2(new Date));// "toString方法被覆盖!"

image-20201028233049911

image-20201028233518306

Результаты очевидны. Первый метод по-прежнему дает правильный результат, а второй — нет! Так почему же это так? Мы знаем, что результатом функции связывания является функция, которая является функцией внутри функции и будет выполняться с задержкой, поэтому естественно думать, что здесь может быть замыкание!因为闭包可以保持内部函数执行时的上下文状态. Однако в современных браузерах call и bind реализованы внутри движка js, и у нас нет возможности отладить! Тем не менее, мы можем понять внутреннюю логику движка через исходный код примерной реализации, предоставленной polly-fill.В этой статье приведена отлаженная демонстрация.Вы можете попробовать:

// 模拟实现call
// ES6实现
Function.prototype.mycall = function (context) {
  context = context ? Object(context) : window;
  var fn = Symbol();
  context[fn] = this;

  let args = [...arguments].slice(1);
  let result = context[fn](...args);

  delete context[fn]
  return result;
}

// 模拟实现bind
Function.prototype.mybind = function (context) {
  if (typeof this !== "function") {
    throw new Error("请使用函数对象调用我,谢谢!");
  }

  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);

  var fNOP = function () { };

  var fBound = function () {
    var bindArgs = Array.prototype.slice.call(arguments);
    return self.myapply(this instanceof fNOP ? this : context, args.concat(bindArgs));
  }

  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
}

// 模拟实现apply
// ES6实现
Function.prototype.myapply = function (context, arr) {
    context = context ? Object(context) : window;
    var fn = Symbol();
    context[fn] = this;
    let result;
    if (!arr) {
        result = context[fn]();
    } else {
        result = context[fn](...arr);
    }

    delete context[fn]
    return result;
}

var toStr1 = Function.prototype.mycall.mybind(Object.prototype.toString);

console.log(toStr1({}));      // "[object Object]"
console.log(toStr1([]));      // "[object Array]"
console.log(toStr1(123));     // "[object Number]"
console.log(toStr1("abc"));   // "[object String]"
console.log(toStr1(new Date));// "[object Date]"

image-20201028234453730

Вышеупомянутая реализация опускает некоторые надежные коды и сохраняет только основную логику. Конкретные детали реализации здесь не объясняются. Если вам интересно, вы можете изучить ее самостоятельно. Из devtools мы видимmybindСформированное замыкание действительно находится в области действия объекта функции toStr1!

Конечно, если у вас есть глубокое понимание цепочки прототипов, этот интересный код также можно написать следующим образом:

var toStr3 = Function.call.bind(Object.prototype.toString);
var toStr4 = Function.call.call.bind(Object.prototype.toString);
var toStr5 = Function.call.call.call.bind(Object.prototype.toString);

// 甚至可以这么写。。。
var toStr6 = (()=>{}).call.bind(Object.prototype.toString);

-END-