[Расширенная фаза 3-4] Углубленный анализ принципа связывания, сценариев использования и реализации моделирования

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

Обновление: Спасибо за вашу поддержку. Недавно я бросил сводку данных для всех, чтобы прочитать систему. В будущем будет больше контента и больше оптимизации.Нажмите здесь, чтобы просмотреть

------ Далее идет текст ------

bind()

bind()метод создает новую функцию, и когда эта новая функция вызывается, ееthisзначение передаетсяbind()Первый параметр , второй и последующие параметры, переданные в метод привязки, плюс параметры самой среды выполнения функции привязки используются в качестве параметров исходной функции для вызова исходной функции. Также можно использовать функцию привязки, возвращаемую функцией bind.newоператора для создания объекта: такое поведение похоже на обращение к исходной функции как к конструктору, обеспечивающемуthisЗначение игнорируется, а аргументы во время вызова передаются фиктивной функции. (из ссылки 1)

грамматика:fun.bind(thisArg[, arg1[, arg2[, ...]]])

bindМетоды иcall / applyСамая большая разница в том, что первый возвращает контекст привязкифункция, а последние двапрямое исполнениефункция.

Проиллюстрируем на примере

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

var bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

var bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}

Из кода выше видно, чтоbindИмеет следующие особенности:

  • 1. Можно указатьthis
  • 2. Вернуть функцию
  • 3. Параметры можно передавать в
  • 4. Карри

сцены, которые будут использоваться

1. Бизнес-сценарии

Часто встречаются следующие бизнес-сценарии

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
//Hello, my name is Kitty

вывод здесьnicknameявляется глобальным, не созданным намиpersonпереданный параметр, потому чтоsetTimeoutВыполняется в глобальной среде (см. [Расширенная фаза 3-1] для тех, кто не понимает), поэтомуthis указывает наwindow.

здесьsetTimeoutТо же самое верно для асинхронных обратных вызовов, таких как обратные вызовы запроса интерфейса.

Ниже приведены два решения.

Решение 1: кешthisстоимость

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {
        
		var self = this; // added
        setTimeout(function(){
            console.log("Hello, my name is " + self.nickname); // changed
        }, 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

Решение 2:использоватьbind

var nickname = "Kitty";
function Person(name){
    this.nickname = name;
    this.distractedGreeting = function() {

        setTimeout(function(){
            console.log("Hello, my name is " + this.nickname);
        }.bind(this), 500);
    }
}
 
var person = new Person('jawil');
person.distractedGreeting();
// Hello, my name is jawil

Идеально!

2. Проверьте, является ли это массивом

【Расширенная фаза 3-3】ПредставленаcallСценарии использования рассматриваются здесь.

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 直接使用 toString()
[1, 2, 3].toString(); 	// "1,2,3"
"123".toString(); 		// "123"
123.toString(); 		// SyntaxError: Invalid or unexpected token
Number(123).toString(); // "123"
Object(123).toString(); // "123"

в состоянии пройтиtoString()чтобы получить тип каждого объекта, но разные объектыtoString()Существуют разные реализации, поэтомуObject.prototype.toString()для обнаружения необходимоcall() / apply()form для вызова, передавая объект для проверки в качестве первого аргумента.

еще одинПроверьте, является ли это массивомметод, программапреимуществоможно использовать сразу после модификацииtoStr.

var toStr = Function.prototype.call.bind(Object.prototype.toString);
function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}
isArray([1, 2, 3]);
// true

// 使用改造后的 toStr
toStr([1, 2, 3]); 	// "[object Array]"
toStr("123"); 		// "[object String]"
toStr(123); 		// "[object Number]"
toStr(Object(123)); // "[object Number]"

Вышеупомянутый метод используется в первую очередьFunction.prototype.callфункция определяетthisзначение, то.bindВозвращает новую функцию, которая всегдаObject.prototype.toStringУстановить как входящий параметр. на самом деле эквивалентноObject.prototype.toString.call().

вот одинпомещениедаtoString()методне распространяется

Object.prototype.toString = function() {
    return '';
}
isArray([1, 2, 3]);
// false

3. Карри

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

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

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

здесь определяетadd函数,它接受一个参数并返回一个新的函数。 передачаaddПосле этого возвращаемая функция запоминается через замыканиеaddпервый параметр . такbindЭто также сценарий использования замыканий.

Реализация моделирования

bind()Функции были добавлены только в ES5, поэтому не все браузеры их поддерживают.IE8и следующие версии не поддерживаются, если вам нужна совместимость, вы можете использовать Polyfill для достижения.

Во-первых, давайте реализуем следующие четыре функции:

  • 1. Можно указатьthis
  • 2. Вернуть функцию
  • 3. Параметры можно передавать в
  • 4. Карри

Первый шаг в симуляции

Для пункта 1 используйтеcall / applyуточнитьthis.

Для пункта 2 используйтеreturnВернуть функцию.

Объединив два предыдущих пункта, можно написать первую версию, код такой:

// 第一版
Function.prototype.bind2 = function(context) {
    var self = this; // this 指向调用者
    return function () { // 实现第 2点
        return self.apply(context); // 实现第 1 点
    }
}

пройти тест

// 测试用例
var value = 2;
var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind2(foo);

bindFoo(); // 1

Имитационная реализация второго шага

Для пункта 3 используйтеargumentsПолучите массив параметров и используйте его какself.apply()второй параметр .

Для точки 4 получить параметры возвращаемой функции, затем объединить их с параметрами точки 3 в массив параметров и использовать какself.apply()второй параметр .

// 第二版
Function.prototype.bind2 = function (context) {

    var self = this;
    // 实现第3点,因为第1个参数是指定的this,所以只截取第1个之后的参数
	// arr.slice(begin); 即 [begin, end]
    var args = Array.prototype.slice.call(arguments, 1); 

    return function () {
        // 实现第4点,这时的arguments是指bind返回的函数传入的参数
        // 即 return function 的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply( context, args.concat(bindArgs) );
    }
}

есть тест:

// 测试用例
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

var bindFoo = bar.bind2(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}

Имитационная реализация третьего шага

Пока большая часть пройдена, но есть еще одна трудность,bindимеет одну из следующих особенностей

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

Проиллюстрируем на примере:

var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'Jack');
var obj = new bindFoo(20);
// undefined
// Jack
// 20

obj.habit;
// shopping

obj.friend;
// kevin

В приведенном выше примере текущий результатthis.valueВыходundefined, который не является глобальнымvalueни одинfooв объектеvalue,это означаетbindизthisобъект недействителен,newПри реализации создается новый объект, на этот разthisуказывает наobj. ([Расширенная фаза 3-1] Вводится принцип реализации нового, и следующий выпуск также будет посвящен введению)

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

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 注释1
        return self.apply(
            this instanceof fBound ? this : context, 
            args.concat(bindArgs)
        );
    }
    // 注释2
    fBound.prototype = this.prototype;
    return fBound;
}
  • Примечание 1:
    • При использовании в качестве конструктора это указывает на экземпляр, в настоящее времяthis instanceof fBoundРезультатtrue, позволяя экземпляру получить значение из связанной функции, т. е. в приведенном выше примере экземпляр будет иметьhabitАтрибуты.
    • При использовании в качестве обычной функции это указывает наwindow, то результатfalse, указывая this связанной функции наcontext
  • Примечание 2: Измените функцию возвратаprototypeдля связанной функцииprototype, экземпляр может наследовать значение в прототипе связанной функции, то есть в приведенном выше примереobjможет быть полученbarна прототипеfriend.

Примечание: Здесь задействованы знания о прототипе, цепочке прототипов и наследовании, вы можете прочитать мою предыдущую статью.

В JavaScript часто используются восемь видов схемы наследования

Четвертый этап имитационного моделирования

Вышесказанное реализуетсяfBound.prototype = this.prototypeЕсть недостаток, прямая модификацияfBound.prototype, он также напрямую изменитthis.prototype.

Давайте протестируем код:

// 测试用例
var value = 2;
var foo = {
    value: 1
};
function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'Jack'); // bind2
var obj = new bindFoo(20); // 返回正确
// undefined
// Jack
// 20

obj.habit; // 返回正确
// shopping

obj.friend; // 返回正确
// kevin

obj.__proto__.friend = "Kitty"; // 修改原型

bar.prototype.friend; // 返回错误,这里被修改了
// Kitty

Решение состоит в том, чтобы использовать пустой объект в качестве посредника, положитьfBound.prototypeНазначить экземпляр пустого объекта (прототипное наследование).

var fNOP = function () {};			// 创建一个空对象
fNOP.prototype = this.prototype; 	// 空对象的原型指向绑定函数的原型
fBound.prototype = new fNOP();		// 空对象的实例赋值给 fBound.prototype

Здесь вы можете использовать ES5 напрямуюObject.create()метод создания нового объекта

fBound.prototype = Object.create(this.prototype);

ноbindа такжеObject.create()Все методы ES5, некоторые браузеры IE (IE Object.create()выполнитьbind, но принцип тот же.

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

// 第四版,已通过测试用例
Function.prototype.bind2 = function (context) {
    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.apply(
            this instanceof fNOP ? this : context, 
            args.concat(bindArgs)
        );
    }

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

Реализация моделирования Шаг 5

Здесь почти то же самое, но есть проблема со звонкомbindэто не функция, вам нужно создать исключение в это время.

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

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

// 第五版
Function.prototype.bind2 = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    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.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

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

[Расширенная фаза 3-2] Решение проблем мышления

// 1、赋值语句是右执行的,此时会先执行右侧的对象
var obj = {
    // 2、say 是立即执行函数
    say: function() {
        function _say() {
            // 5、输出 window
            console.log(this);
        }
        // 3、编译阶段 obj 赋值为 undefined
        console.log(obj);
        // 4、obj是 undefined,bind 本身是 call实现,
        // 【进阶3-3期】:call 接收 undefined 会绑定到 window。
        return _say.bind(obj);
    }(),
};
obj.say();

[Продвинутая фаза 3-3] Решение проблем мышления

callРеализация моделирования выглядит следующим образом, есть ли проблемы?

Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn;
    return result;
}

Проблема конечно есть, собственно тут и предполагаетсяcontextСам объект неfnсвойства, это точно не сработает, мы должны убедиться, чтоfnУникальность объекта.

Реализация моделирования под ES3

Решение тоже очень простое, сначала судитеcontextСуществует ли недвижимость вfn, если он существует, сгенерировать случайный атрибутfnxx, а затем зациклить запросcontext Существует ли свойство в объектеfnxx. Возвращает конечное значение, если оно не существует.

своего родаКруговая схемаКод реализации выглядит следующим образом:

function fnFactory(context) {
	var unique_fn = "fn";
    while (context.hasOwnProperty(unique_fn)) {
    	unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    
    return unique_fn;
}

своего родарекурсивная схемаКод реализации выглядит следующим образом:

function fnFactory(context) {
	var unique_fn = "fn" + Math.random();
    if(context.hasOwnProperty(unique_fn)) {
        // return arguments.callee(context); ES5 开始禁止使用
        return fnFactory(context); // 必须 return
    } else {
        return unique_fn;
    }
}

Реализация моделированияПолный код выглядит следующим образом:

function fnFactory(context) {
	var unique_fn = "fn";
    while (context.hasOwnProperty(unique_fn)) {
    	unique_fn = "fn" + Math.random(); // 循环判断并重新赋值
    }
    
    return unique_fn;
}

Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    var fn = fnFactory(context); // added
    context[fn] = this; // changed

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context[fn](' + args +')'); // changed

    delete context[fn]; // changed
    return result;
}

// 测试用例在下面

Реализация моделирования под ES6

ES6 имеет новый базовый типSymbol,Выражатьуникальныйзначение, использование заключается в следующем.

const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1); // "symbol"
console.log(symbol3.toString()); // "Symbol(foo)"
console.log(Symbol('foo') === Symbol('foo')); // false

нельзя использоватьnewкоманду, потому что это базовый тип значения, иначе будет сообщено об ошибке.

new Symbol();
// TypeError: Symbol is not a constructor

Реализация моделированияПолный код выглядит следующим образом:

Function.prototype.call = function (context) {
  context = context ? Object(context) : window; 
  var fn = Symbol(); // added
  context[fn] = this; // changed

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

  delete context[fn]; // changed
  return result;
}
// 测试用例在下面

Тестовый случай здесь:

// 测试用例
var value = 2;
var obj = {
    value: 1,
    fn: 123
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(null); 
// 2

console.log(bar.call(obj, 'kevin', 18));
// 1
// {value: 1, name: "kevin", age: 18}

console.log(obj);
// {value: 1, fn: 123}

расширяться

Существует два сценария определения наличия атрибута в объекте.

var obj = {
     a: 2
};
Object.prototype.b = function() {
    return "hello b";
}
  • 1,inоператор

inОператор проверяет свойство на наличие объекта и его[[Prototype]]в цепочке прототипов.

("a" in obj);     // true
("b" in obj);     // true
  • 2,Object.hasOwnProperty(...)метод

hasOwnProperty(...)будет только проверять, существует ли свойство в объекте,Не будетПроверьте его цепочку прототипов.

obj.hasOwnProperty("a");     //true
obj.hasOwnProperty("b");     //false

УведомлениеСледующие пункты:

  • 1. ВыглядитinОператор может проверить, есть ли в контейнере определенное значение, на самом деле это определенное значениеИмя свойстваон существует. Для массивов4 in [2, 4, 6]результат возвращенfalse,потому что[2, 4, 6]Имена свойств, содержащиеся в этом массиве,0,1,2,нет4.
  • 2. Все обычные предметы можно пройти черезObject.prototypeделегировать доступhasOwnProperty(...), а для некоторых специальных объектов (Object.create(null)создан) не подключен кObject.prototype, и в этом случае вы должны использоватьObject.prototype.hasOwnProperty.call(obj, "a"), показывая привязанный кobjначальство.Еще одинcallПрименение.

Вопросы для размышления по этому выпуску

Реализовать функцию бесконечного накопления с помощью JSadd, пример следующий:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

// 以此类推

Ссылаться на

Макет метода привязки ES5 без методов вызова и применения

Углубленная имитационная реализация bind в JavaScript

Function.prototype.bind() из MDN

Символ MDN

Глава 4: Каррирование