3000 слов, и я не написал Function.prototype.call

внешний интерфейс

предисловие

Function.prototype.call, серия рукописного ввода, серия интервью Wanwen и контент, который должен быть включен в обязательную серию, что показывает его вес во внешнем интерфейсе.
Эта статья основана наMDNа такжеECMAСтандарты, и заново со всеми знакомимсяcall.

Задействованные точки знаний:

  1. undefined
  2. недействительный унарный оператор
  3. строгий режими нестрогий режим
  4. Распознавание среды браузера и nodejs
  5. побочные эффекты функции (чистые функции)
  6. eval
  7. Content-Security-Policy
  8. delete
  9. new Function
  10. Object.freeze
  11. Проверка свойств объекта
  12. Сцена интервью
  13. Отношения любви и ненависти между спецификациями ECMA и поставщиками браузеров

Наггетс популярная версия

Вопросы интервьюера:
Пожалуйста, напишите это от рукиFunction.prototype.call

на основеES6的拓展运算符Версия

Function.prototype.call = function(context) {
    context = context || window;
    context["fn"] = this;
    let arg = [...arguments].slice(1); 
    context["fn"](...arg);
    delete context["fn"];
}

Эта версия не должна быть реальным ответом, которого хочет интервьюер. Не делайте много разбора.

на основеevalверсия

Function.prototype.call = function (context) {
  context = (context == null || context == undefined) ? window : new  Object(context);
  context.fn = this;
  var arr = [];
  for (var i = 1; i < arguments.length; i++) {
    arr.push('arguments[' + i + ']');
  }
  var r = eval('context.fn(' + arr + ')');
  delete context.fn;
  return r;
}

Эта версия заслуживает улучшения

  1. thisНе оценивается ли функция
  2. Используйте undefined, чтобы судить, безопасный и небезопасный
    undefined может быть перезаписан (браузеры более высоких версий наложили ограничения).
  3. Использование окна напрямую в качестве контекста по умолчанию слишком произвольно.
    Среда выполнения скрипта, браузер? узлы?
    Режим работы функции, строгий режим, нестрогий режим?
  4. evalРазрешат ли его казнить?
  5. Имеет ли удаление context.fn побочные эффекты?
    Что делать, если в контексте есть атрибут fn?

прежде чем мы действительно начнем писатьFunction.prototype.callПеред этим давайте посмотрим, как ее определяют MDN и ECMA.

Описание вызова MDN

грамматика

function.call(thisArg, arg1, arg2, ...)

параметр

thisArg

необязательный. Это значение, используемое при выполнении функции function. Обратите внимание, что это может не быть фактическим значением, видимым методом: если эта функция находится вв нестрогом режиме, если он указан как null или undefined, он будет автоматически заменен указателем наглобальный объект,Примитивные значения завернуты.
arg1, arg2, ...
Указанный список параметров.

раскрытая информация

Здесь раскрывается несколько фрагментов информации, которые я выделил жирным шрифтом:

  1. Нестрогий режим, соответствующий строгому режиму
  2. Здесь говорится, что он указывает на глобальный объект, а не говорит «да».window. Конечно, говорят, что MDN не слишком большой. Я хочу добавить это.nodejsТакже реализует стандарт ES. Итак, когда мы внедряем, должны ли мы учитыватьnodejsЧто насчет окружающей среды.
  3. Примитивные значения завернуты. какая упаковка?Object(val), т.е. завершение исходного значенияvalпакет из.

стандарт ЕС

существуетFunction.prototype.call() - JavaScript | MDNВерсия спецификации ES указана внизу, каждая версия имеетcall实现的проиллюстрировать.

То, что мы реализуем, основано на определенной версии ES.

Поскольку версия ES отличается, детали реализации могут отличаться, и среда реализации также отличается.

Версия спецификации условие иллюстрировать
ECMAScript 1st Edition (ECMA-262) Standard Начальное определение. Реализовано в JavaScript 1.3.
ECMAScript 5.1 (ECMA-262)
Function.prototype.call
Standard
ECMAScript 2015 (6th Edition, ECMA-262)
Function.prototype.call
Standard
ECMAScript (ECMA-262)
Function.prototype.call
Living Standard

В стандарте ES3 оcallВ спецификации указано11.2.3 Function Calls, вы можете найти его, выполнив прямой поиск.

Сегодня мы в основном основаны на стандарте ES5 2009 года.Function.prototype.call, Некоторые люди могут сказать, почему бы вам не реализовать это в соответствии со стандартом ES3, потому что ES5 может потребовать больше очков знаний.

ненадежный неопределенный

(context == null || context == undefined) ? window : new Object(context)

код вышеundefinedНе обязательно надежный.

Процитирую отрывок из MDN:

В современных браузерах (JavaScript 1.8.5/Firefox 4+) undefined является ненастраиваемым и недоступным для записи свойством, начиная со стандарта ECMAscript5. Даже если это не так, не переписывайте его.

использовать без контекстаvoid 0чем использовать напрямуюundefinedБезопаснее.

Некоторые студенты, возможно, не видели ситуации, когда undefined переписывается, ничего страшного, вот картинка:

image.png

voidЭта унарная операция, за исключением этого, готовится вернутьundefinedКроме того, есть два других распространенных применения:

  1. href тега ничего не делает
    <a href="javascript:void(0);">

  2. IIFE выполняется немедленно

;void function(msg){
    console.log(msg)
}("你好啊");

Конечно, более прямой путь:

;(function(msg){
    console.log(msg)
})("你好啊");

Распознавание среды браузера и nodejs

Среда браузера:

typeof self == 'object' && self.self === self

среда nodejs:

typeof global == 'object' && global.global === global

теперь естьglobalThis, поддерживаются в браузерах высоких версий и nodejs.

Очевидно, что в нашем сценарии его пока нельзя использовать, но его идеи можно использовать для справки:

var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

строгий режим

Поддерживает ли строгий режим

Strict modeСтрогий режим — это характеристики, введенные ES5. Тогда как нам убедиться, что ваша среда поддерживает строгий режим?

var hasStrictMode = (function(){ 
 "use strict";
 return this == undefined;
}());

вернется нормальноtrue, поместите его в IE8 и выполните:

image.png

В нестрогом режиме контекст вызова функции (значение this) является глобальным объектом. В строгом режиме контекст вызова не определен.

находится в строгом режиме

Знать, поддерживается ли строгий режим, недостаточно, нам также нужно знать, находимся ли мы в строгом режиме.

Следующий код может определить, находится ли он в строгом режиме:

var isStrict = (function(){
  return this === undefined;
}());

Этот код работает в браузерах, поддерживающих строгий режим иnodejsработать в любой среде.

побочные эффекты функции

  var r = eval('context.fn(' + arr + ')');
  delete context.fn;

Приведенный выше код напрямую удаляет контекст наfnатрибут, если исходный контекст имеетfnатрибут, он будет потерян?

Мы используемevalверсияcall, выполните следующий код

var context = {
  fn: "i am fn",
  msg: "i am msg"
}

log.call(context);  // i am msg

console.log("msg:", context.msg); // i am msg
console.log("fn:", context.fn); // fn: undedined

можно увидеть контекстfnАтрибут был убит, что уничтожает входные параметры и имеет побочные эффекты, которые не должны производиться.
Аналог побочных эффектов находится в функциональном программировании.чистая функция.

Мы должны принять соответствующие действия, в основном две идеи:

  1. Создайте свойство, которое не будет иметь такое же имя
  2. Сохранить сцену и восстановить сцену

Возможны оба варианта, но я думаю, что вариант 2 проще и легче реализовать:
Основной код выглядит следующим образом:

var ctx = new Object(context);

var propertyName = "__fn__";
var originVal;
var hasOriginVal = ctx.hasOwnProperty(propertyName)
if(hasOriginVal){
    originVal = ctx[propertyName]
}

...... // 其他代码

if(hasOriginVal){
    ctx[propertyName] = originVal;
}
  

на основеevalРеализация в основном следующая

основанный на стандартахECMAScript 5.1 (ECMA-262) Function.prototype.call

When the call method is called on an object func with argument thisArg and optional arguments arg1, arg2 etc, the following steps are taken:

1. If IsCallable(func) is false, then throw a TypeError exception.

2. Let argList be an empty List.

3. If this method was called with more than one argument then in left to right 
order starting with arg1 append each argument as the last element of argList

4. Return the result of calling the [[Call]] internal method of func, providing 
thisArg as the this value and argList as the list of arguments.
The length property of the call method is 1.

NOTE The thisArg value is passed without modification as the this value. This is a 
change from Edition 3, where a undefined or null thisArg is replaced with the  
global object and ToObject is applied to all other values and that result is passed 
as the this value.

Для нас важнее1а такжеNote:

Взгляните на нашу базовую реализацию

var hasStrictMode = (function () {
    "use strict";
    return this == undefined;
}());

var isStrictMode = function () {
    return this === undefined;
};

var getGlobal = function () {
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};

function isFunction(fn){
    return typeof fn === "function";
}

function getContext(context) {

    var isStrict = isStrictMode();

    if (!hasStrictMode || (hasStrictMode && !isStrict)) {
        return (context === null || context === void 0) ? getGlobal() : Object(context);
    }

    // 严格模式下, 妥协方案
    return Object(context);
}

Function.prototype.call = function (context) {

    // 不可以被调用
    if (typeof this !== 'function') {
        throw new TypeError(this + ' is not a function');
    }
    
    // 获取上下文
    var ctx = getContext(context);

    // 更为稳妥的是创建唯一ID, 以及检查是否有重名
    var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
    var originVal;
    var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
    if (hasOriginVal) {
        originVal = ctx[propertyName]
    }
    
    ctx[propertyName] = this;    

    // 采用string拼接
    var argStr = '';
    var len = arguments.length;
    for (var i = 1; i < len; i++) {
        argStr += (i === len - 1) ? 'arguments[' + i + ']' : 'arguments[' + i + '],'
    }
    var r = eval('ctx["' + propertyName + '"](' + argStr + ')');

    // 还原现场
    if (hasOriginVal) {
        ctx[propertyName] = originVal;
    } else {
        delete ctx[propertyName]
    }

    return r;
}

Текущая версия все еще имеет проблемы.

  1. В строгом режиме мы по-прежнему используемObejectупакованный.

Это приведет к неточности этому указателю при прохождении некселей в строгом режиме, который не может быть скомпрометирован.Какой одноклассник имеет лучшее решение, пожалуйста, руководство.

  1. Хотя мы усложнили переименование имени временного свойства, если имя такое же, и этот метод действительно вызывается в вызове функции, это может привести к ненормальному поведению.

Таким образом, идеальным решением является создание UID.

  1. evalреализация, может бытьContent-Security-PolicyНе допустить

Общая оперативная информация выглядит следующим образом:

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an   
allowed source of script in the following Content Security Policy directive: "script-src 
.........

image.png

Первые два должны быть приемлемыми, что касается третьего, то мы не можем идти на компромисс.

Это должно пригласить следующего гостя,new Function.

new Function

new Function ([arg1[, arg2[, ...argN]],] functionBody)

Основной формат такой же, как указано выше, а последний — тело функции.

Возьмем простой пример:

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// expected output: 8

насcallКоличество параметров не фиксировано, идея состоит в том, чтобы начать сargumentsДинамическое приобретение.

Здесь наша реализация заимствуетИнтервьюер спросил: Могу ли я смоделировать вызов и применить методы JS?Реализация:

function generateFunctionCode(argsArrayLength){
    var code = 'return arguments[0][arguments[1]](';
    for(var i = 0; i < argsArrayLength; i++){
        if(i > 0){
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    // return arguments[0][arguments[1]](arg1, arg2, arg3...)
    return code;
}

Реализация на основе новой функции

var hasStrictMode = (function () {
    "use strict";
    return this == undefined;
}());

var isStrictMode = function () {
    return this === undefined;
};

var getGlobal = function () {
    if (typeof self !== 'undefined') { return self; }
    if (typeof window !== 'undefined') { return window; }
    if (typeof global !== 'undefined') { return global; }
    throw new Error('unable to locate global object');
};

function isFunction(fn){
    return typeof fn === "function";
}

function getContext(context) {
    var isStrict = isStrictMode();

    if (!hasStrictMode || (hasStrictMode && !isStrict)) {
        return (context === null || context === void 0) ? getGlobal() : Object(context);
    }
    // 严格模式下, 妥协方案
    return Object(context);
}


function generateFunctionCode(argsLength){
    var code = 'return arguments[0][arguments[1]](';
    for(var i = 0; i < argsLength; i++){
        if(i > 0){
            code += ',';
        }
        code += 'arguments[2][' + i + ']';
    }
    code += ')';
    // return arguments[0][arguments[1]](arg1, arg2, arg3...)
    return code;
}


Function.prototype.call = function (context) {

    // 不可以被调用
    if (typeof this !== 'function') {
        throw new TypeError(this + ' is not a function');
    }

    // 获取上下文
    var ctx = getContext(context);

    // 更为稳妥的是创建唯一ID, 以及检查是否有重名
    var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
    var originVal;
    var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
    if (hasOriginVal) {
        originVal = ctx[propertyName]
    }

    ctx[propertyName] = this;
 
    var argArr = [];
    var len = arguments.length;
    for (var i = 1; i < len; i++) {
        argArr[i - 1] = arguments[i];
    }

    var r = new Function(generateFunctionCode(argArr.length))(ctx, propertyName, argArr);

    // 还原现场
    if (hasOriginVal) {
        ctx[propertyName] = originVal;
    } else {
        delete ctx[propertyName]
    }

    return r;
}

Сбор вопросов в комментариях

Лучшее в комментариях:

  1. почему бы нетSymbol

Поскольку он написан на основе стандарта ES5, если вы используетеSymbol, также можно использовать оператор распространения. Знаний следствия, естественно, гораздо меньше.

  1. Уровень апплета Alipay и новая функция недоступны

В этом случае это может быть действительно невозможно.

  1. Объекты после Object.freeze не могут добавлять свойства

благодарныйНанакопоправка, его статьяРазница между почерком и вызовом нативной функции .prototye.CallРекомендую всем внимательно прочитать.

Следующий код сообщит об ошибке в строгом режиме, а копирование в нестрогом режиме завершится ошибкой:

"use strict";
var context = {
    a: 1,
    log(msg){
        console.log("msg:", msg)
    }
};

Object.freeze(context);
context.fn = function(){

};

console.log(context.fn);

VM111 call:12 Uncaught TypeError: Cannot add property fn, object is not extensible
    at VM49 call:12

Как это сделать, я думаю двумя способами:

  1. копировать объект
  2. Obect.create

Это тоже компромиссный метод, ведь ссылка все же длиннее.

"use strict";
var context = {
    a: 1,
    log(msg){
        console.log("msg:", msg)
    }
};

Object.freeze(context);

var ctx =  Object.create(context);

ctx.fn = function(){

}

console.log("fn:", typeof ctx.fn);  // fn: function
console.log("ctx.a", ctx.a);  // ctx.a 1
console.log("ctx.fn", ctx.fn); // ctx.fn ƒ (){}

резюме

Просмотрите проблемы, которые все еще существуют

  1. В строгом режиме нам все еще нужно использоватьObjectИнкапсулированные базовые типы данных

Это приведет к неточности этого указателя при передаче не-объектов в строгом режиме, который не может быть скомпрометирован.У кого из студентов есть лучший план, пожалуйста, помогите.

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

  2. Использование небольших программ и других сред может быть запрещеноevalа такжеnew Function

  3. объект заморожен,callв исполнительной функцииthisнетнастоящийПереданный объект контекста.

Итак, я изменил название3000 слов, и я не написал Function.prototype.call

Сцена интервью

почеркcallЭто требует много очков знаний, а мой уровень ограничен.Если есть какие-то упущения, прошу понять и дополнить.

Когда интервьюер задает вопросы, вы должны четко понимать, на какую должность вы проходите собеседование, будь то P6, P7 или P8.
Это старший разработчик или фронтенд-лидер, или фронтенд-лидер.
В зависимости от позиции интервьюер, конечно, будет ожидать разных ответов.

напиши в конце

Ссылки и цитаты

Function.prototype.call() - JavaScript | MDN
Strict mode - JavaScript | MDN
ECMAScript 5 Strict Mode
Коллекция ЕС
Рукописный вызов, подача заявки, привязка реализации и подробное объяснение
Принцип реализации вызова, применения и привязки
Интервьюер спросил: Могу ли я смоделировать вызов и применить методы JS?