blog

this」に対するフロントエンドの包括的な攻撃について再認識する。

最近、私はフロントエンドを学び直すシリーズを書いています。\nフロントエンドのプロトタイプ/チェーンと継承の再確認。\nフロントエンドのクロージャとモジュール\n\nフロントエンドの実行メカニズム\n...

Feb 28, 2020 · 22 min. read
シェア

結合ルール

関数の実行中に呼び出される場所によって、thisが誰にバインドされるかがどのように決まるかを見てみましょう。

通話の場所を特定し、次の4つのルールのうち、どれを適用する必要があるかを判断する必要があります。4つのルールのそれぞれについて最初に説明し、次に、複数のルールが利用可能な場合に、それらのルールがどのように優先されるかを説明します。

これ」には5つの縛りがあります:

  1. デフォルトバインディング
    1. ストリクト・モード
    2. 非厳密モード
  2. 暗黙のバインディング
  3. 明示的バインディング
    1. bind
    2. call
    3. apply
  4. new

デフォルトのバインディング

デフォルトのバインディングは、その名の通り、誰も欲しがらない「孤児」に適用されます。

そのコードが何を出力するか考えてみてください。

var a = 'out'
function fnc() {
 var a = 'in'
 console.log(this.a)
}
fnc();

答えは『アウト』です

fnc()が呼び出されると、this.aがグローバル変数aに解決されることがわかります。この場合、関数呼び出しは this のデフォルト・バインディングを適用するので、this はグローバル・オブジェクトを指します。"はグローバル・オブジェクトを指します。

では、なぜ彼がデフォルトのバインディングを適用しているとわかるのですか?

fncでは、彼は修飾語**なしで単独で呼ばれるだけなので、孤児院に行かなければならない望まれない子供と同等であることがわかりました - デフォルトのバインディング。

デフォルト・バインディングが何なのか、はっきりしないかもしれません。後でルールが紹介されると、他のルールを適用していない「孤児」たちは、デフォルト・バインディングの懐にある孤児院にしか来ることができないことがわかります。

厳密モード

strict の場合、デフォルトのバインディングは window ではなく undefined です。

var a = 'out'
function fnc() {
	"use strict";
 var a = 'in'
 console.log(this.a)
}
fnc();

答えは「エラー報告

なぜかって?undefinedでundefinedを探そうとすると、確実にエラーになるからです。

暗黙のバインディング

インプリシット・ボンディングとは、その名の通り、静かな絆、つまり認知はされているが明確な絆ではない絆のことです。男女が手をつないで歩いているようなもので、基本的にはカップルと見なされますが、"浮気 "の可能性もあります。

そのコードが何を出力するか考えてみてください。

var a = 'out'
function fnc() {
 console.log(this.a)
}
var obj = {
 a: 'in',
 fnc: fnc
}
obj.fnc();

答えは『中』です

fncはどのような状況で呼ばれるのでしょうか?つまり、fncはすでにobjに所有されており、fncが言うことはobjに有利に違いなく、わざわざ何かを言うことはありません。

実は、ここで少し注意すべき点があります。それは、複数のオブジェクト参照によって呼び出しが行われた場合、この時点では誰の実行コンテキストなのかということです。

実際、この問題は常識的に説明できます。AはBに食事に行こうと誘いましたが、BはCに一緒に行こうと誘いたいと言い、AとBが対立することになりました。それならBでしょう。Cがあなたを誘ったからこそ、あなたたちはこのパーティーに現れたのですから。ですから、この質問に答えるのは難しくありません。例を直接見てください。

var name = 'aa'
function fnc() {
 console.log(this.name)
}
var bb = {
 name: 'bb',
 fnc: fnc
}
var cc = {
 name: 'cc',
 bb: bb
}
cc.bb.fnc();

答えはbb

fncが呼ばれるのはbbだから。なぜなら、fncを呼ぶことができるのはbbであり、bbがなければfncが登場するチャンスはないからです。

暗黙的に失われる

先ほど申し上げたように、表の上では多くのことが正常に見えますが、他の条件が発生する可能性があります。例えば、暗黙の損失における暗黙のバインディング。

例を修正

var name = 'aa'
function fnc() {
 console.log(this.name)
}
var bb = {
 name: 'bb',
 fnc: fnc
}
var cc = {
 name: 'cc',
 bb: bb
}
var aa = cc.bb.fnc;
aa()

答えはaa

なぜでしょう?bbはfncの信頼を失い、fncは孤児院に行くしかないのです。

さっきの食事の例でも、シナリオを修正するとわかりやすいですよ。

Aは食事に行こうとBに電話したところ、Bが誤ってCの連絡先を紛失してしまい、それを見知らぬDが拾いました。たまたまDも合コンに行くことになったので、Cは合コンに参加しようとDに電話したのです。しかし、CはDを知らないし、電話をかけてきたのは知人のBでもないので、誰も信用できません。結局、デフォルト・ルール、デフォルト・バインディングを適用するしかないのです。

もうひとつ、暗黙のうちに失われがちなケースとして、コールバック関数があります。

以下の例をご覧ください:

var name = 'window'
function fnc() {
 console.log(this.name)
}
function ffnncc(fn) {
 fn()
}
var obj = {
 name: 'obj',
 fnc: fnc,
}
ffnncc(obj.fnc)

答えは「今」です。

なぜでしょう?objがそれを呼び出したように見えますが、実際にはobjはfncを呼び出したメソッドを、fncが認識していない他の誰かに渡しています。

var name = 'window'
function fnc() {
 console.log(this.name)
}
var obj = {
 name: 'obj',
 fnc: fnc,
}
setTimeout(obj.fnc, 100)

答えは「今」です。

fncの連絡先の詳細は、objによって他の誰かに渡されました!

ハハハハ、"これ "って簡単なことなんですか?

考える材料:では、なぜreactの関数呼び出しはアロー関数を使うのか、なぜコンストラクタ内でバインドする必要があるのか。

明示的バインディング

今ご覧いただいたのは、暗黙的なバインディング、卑劣なバインディングです。次は明示的なバインディングです。

オブジェクトの中に関数参照を入れたくない場合、オブジェクトに関数呼び出しを強制したい場合、どうすればいいでしょうか?

それがどうしたんですか?

JavaScriptの "すべての "関数には、この問題を解決するために使える便利な機能があります。具体的には、関数のcall(...) やapply(...) メソッドを使うことができます。技術的に言えば、JavaScriptのホスト環境は、この2つのメソッドを持たない非常に特殊な関数を提供することがあります。しかし、そのような関数は非常にまれで、JavaScriptが提供するほとんどの関数や、あなたが作成するすべての関数は、call(...)メソッドとapply(...)メソッドを使用することができます。 とapply(...) メソッドを使うことができます。

以下の例をご覧ください:

var a = 'window'
function foo() { 
	console.log( this.a );
}
var obj = { 
	a:'obj'
};
foo.call( obj );
foo()

答えの順番は、obj、windowです。

foo.call(...)を使えばを使うことで、fooを呼び出すときに、その thisをobjにバインドさせることができます。

残念ながら、明示的バインディングでは、バインディングが失われるという問題は解決されません。

function sayHi(){
 console.log('Hello,', this.name);
}
var person = {
 name: 'YvetteLau',
 sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
 fn();
}
Hi.call(person, person.sayHi); 

答えは「こんにちは、ウィリアム。

fnが直接呼び出されているわけではないので、暗黙の損失を引き起こしていることはわかりにくいことではありません。この呼び出しにバインドされている人は実際にはHiにバインドされています。

function sayHi(){
 console.log('Hello,', this.name);
}
var person = {
 name: 'YvetteLau',
 sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
 	console.log(this.name)
 fn();
}
Hi.call(person, person.sayHi); 

出力は?

イヴェットラウ

こんにちは、ウィリアム

では、どうすればいいのでしょうか?ハードバインディングはこの問題を解決することができます。

ハード・バインディング

ハード・バインディングは、実は最後のレイヤーにディスプレイ・バインディングを施しているのです。

コードをご覧ください:

function sayHi(){
 console.log('Hello,', this.name);
}
var person = {
 name: 'YvetteLau',
 sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
 	console.log(this.name)
 fn.call(person);
}
Hi(person.sayHi); 

出力は?

イヴェットラウ

こんにちは、ウィリアム

Hiの呼び出しは依然としてcall thisのウィンドウを指していますが、callでpersonにバインドしているため、その後に関数fnがどのように呼び出されても、常に手動でpersonにfnを呼び出します。このようなバインディングは明示的に強制されるため、ハードバインディングと呼ばれます。

function foo() {
 console.log( this.a );
}
var obj = {
 a: 2
};
var bar = function() {
 foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// ハード・バインド・バーの thisを変更することはできない。
bar.call( window ); // 2

典型的なアプリケーションのシナリオは、引数を受け取って値を返す役割を持つラップ関数を作成することです。

function foo(something) {
 console.log( this.a, something );
 return this.a + something;
}
var obj = {
 a: 2
};
var bar = function() {
 return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5

再利用可能なヘルパー関数を作成します。

function foo(something) {
 console.log( this.a, something );
 return this.a + something;
}
// 単純な補助バインディング
function bind(fn, obj) {
 return function() {
 return fn.apply( obj, arguments );
 }
}
var obj = {
 a: 2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

Function.prototype.bindES5には組み込みの , があり、bindは以下のような用法でハードバインドされた新しい関数を返します。

function foo(something) {
 console.log( this.a, something );
 return this.a + something;
}
var obj = {
 a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5

API呼び出しの「コンテキスト」。

JSの組み込み関数の多くには、bind(...)のように動作する「コンテキスト」と呼ばれるオプションのパラメータが用意されています。これは、bind(...)と同様に、コールバック関数が指定された thisを使用することを保証します。これらの関数は、実際には call(...)や apply(...)や apply(...) を通して呼び出されます。

function foo(el) {
	console.log( el, this.id );
}
var obj = {
 id: "awesome"
}
var myArray = [1, 2, 3]
// foo()を呼び出す..)this "をobjにバインドする場合
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

new

newを使って関数が呼び出されたり、コンストラクタが呼び出されたりすると、自動的に以下の処理が行われます。

  1. 新しいオブジェクトを作成します。
  2. この新しいオブジェクトは[[プロトタイプ]]接続を行います。
  3. この新しいオブジェクトは、関数呼び出しの thisにバインドされます。
  4. その関数が他のオブジェクトを返さない場合、new式の関数呼び出しは自動的にこの新しいオブジェクトを返します。

次のコードを考えてみてください。

var a = 4
function foo(a) { 
 this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2

foo(...)を呼び出すためにnewを使用します。を呼び出すと、新しいオブジェクトが構築され、foo(...) の this にバインドされます。で this にバインドします。

バインディングの優先順位

このルールを読んだ後、このルールが衝突した場合、誰の言うことを聞けばいいのだろうと考えざるを得ません。

デフォルトのバインディングが4つのルールの中で最も優先順位が低いのは間違いありません。ですから、今は無視してかまいません。 暗黙的バインディングと明示的バインディングのどちらが優先順位が高いでしょうか?テストしてみましょう。

function foo() { 
	console.log( this.a );
}
var obj1 = { 
	a: 2,
	foo: foo 
};
var obj2 = { 
	a: 3,
	foo: foo 
};
obj1.foo(); // 2 
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3 
obj2.foo.call( obj1 ); // 2

見てわかるように、明示的バインディングの方が優先順位が高いので、判断するときは明示的バインディングを先に適用できるかどうかを考える必要があります。 ここで、新しいバインディングと暗黙のバインディングのどちらが優先順位が高いか低いかを考える必要があります。

function foo(something) { 	
	this.a = something;
}
var obj1 = { 
	foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 ); 
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 ); 
console.log( obj1.a ); // 2 
console.log( bar.a ); // 4

新しいバインディングは暗黙のバインディングよりも優先順位が高いことがわかります。しかし、新しいバインドと明示的なバインドのどちらが優先順位が高いでしょうか?

newとcall/applyは一緒に動作しないので、new foo.call(obj1)で直接テストすることはできません。しかし、ハードバインディングを使って、両方の優先順位をテストすることはできます。

これは、関数の中に[[Call]]と[[Constructor]]という2つの異なるメソッドがあるからです。 通常の関数で呼び出された場合は[[Call]]が実行されます。call、apply、bind、arrow関数は内部に[[Constructor]]メソッドを持ちません。


function foo(something) { 
	this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 ); 
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3); 
console.log( obj1.a ); // 2 
console.log( baz.a ); // 3

バインディングのハードなバインディングの上に新しいトップがあるのがわかります。

新品>ハードカバー

概要

新しいバインディング > バインディングの表示 > 暗黙のバインディング > デフォルトのバインディング

バインディング例外

無視される this.

call や apply、bind に this のバインディングオブジェクトとして null や undefined を渡すと、呼び出し時にこれらの値は無視され、デフォルトのルールが実際に適用されます。

以下の2つのケースはNULLを通過します。

  • apply(...)を使用して配列を "展開" し、それを関数のパラメータとして渡します。
  • bind(...)引数をコリエイトすることも可能です。
function foo(a, b) {
 console.log( "a:" + a + " :" + b );
}
// 配列をパラメータに展開する
foo.apply( null, [2, 3] ); // a:2 :3
// bind()を使う..)コリアライズを行う
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2 :3 

this "バインディングを無視するために常にnullを渡すことは、いくつかの副作用があります。関数が this を使用している場合、デフォルトのバインディングルールは this をグローバルオブジェクトにバインドします。

より安全な this。

安全なのは特別なオブジェクトを渡すことで、そのオブジェクトに thisをバインドしてもアプリケーションに副作用はありません。

JSでnullオブジェクトを作成する最も簡単な方法は**Object.create(null)**で、これは{}とよく似ていますが、デリゲートのObject.prototypeを作成しないので、{}よりもnullです。

function foo(a, b) {
 console.log( "a:" + a + " :" + b );
}
//  
var ø = Object.create( null );
// 配列をパラメータに展開する
foo.apply( ø, [2, 3] ); // a:2 :3
// bind()を使う..)コリアライズを行う
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2 :3 

間接参照

間接参照では、この関数を呼び出すとデフォルトのバインディング・ルールが適用されます。間接参照は、代入時に発生する可能性が最も高いです。

// p.foo = o.fooコンストラクタの戻り値はターゲット関数への参照なので、pの代わりにfoo()と呼ばれる。.foo() .foo()
function foo() {
 console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};
o.foo(); // 3
(p.foo = o.foo)(); // 2

ソフト・バインディング

  • ハード・バインディングを使用すると、this を指定したオブジェクトに強制的にバインドすることができ、関数の呼び出しでデフォルトのバインディング・ルールが適用されるのを防ぐことができます。しかし、これは関数の柔軟性を低下させ、ハードバインディングを使用した後に暗黙的または明示的なバインディングを使用してthisを変更することはできません。
  • グローバルオブジェクトを指定し、デフォルトバインディングに undefined 以外の値を指定すると、ハードバインディングと同じ効果が得られます。
// デフォルトのバインディング・ルール、最後の優先順位
// this "がグローバル・オブジェクトにバインドされているか未定義の場合、指定されたデフォルト・オブジェクトobjが thisにバインドされ、そうでなければ thisは変更されない。"
if(!Function.prototype.softBind) {
 Function.prototype.softBind = function(obj) {
 var fn = this;
 // すべてのcurried引数をキャプチャする
 var curried = [].slice.call( arguments, 1 ); 
 var bound = function() {
 return fn.apply(
 	(!this || this === (window || global)) ? 
 	obj : this,
 curried.concat.apply( curried, arguments )
 );
 };
 bound.prototype = Object.create( fn.prototype );
 return bound;
 };
}

使用法: ソフトバインド・バージョンの foo() は、this を手動で obj2 または obj3 にバインドできますが、デフォルトのバインドが適用されると、this は obj にバインドされます。

function foo() {
 console.log("name:" + this.name);
}
var obj = { name: "obj" },
 obj2 = { name: "obj2" },
 obj3 = { name: "obj3" };
// デフォルト・バインディング。thisをデフォルト・オブジェクトobjにバインドするソフト・バインディングを適用する。
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj 
// 暗黙の結合規則
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 <---- 見て!
// 明示的な結合ルール
fooOBJ.call( obj3 ); // name: obj3 <---- 見て!
// バインディングは失われ、ソフトバインディングが適用される
setTimeout( obj2.foo, 10 ); // name: obj

矢印関数

ES6では、アロー関数という特殊な関数が追加されました。アロー関数は、上記の4つのルールを使うことができませんが、その代わりに、外側のスコープ(レキシカルスコープ)に基づいてthisを決定します。

  • foo()内に作成されたarrow関数は、呼び出されたときにfoo()の thisを取り込みます。foo()の thisはobj1にバインドされているので、barの thisもobj1にバインドされ、アロー関数のバインディングは変更できません
function foo() {
 // 矢印関数を返す
 return (a) => {
 // thisfoo()から継承された
 console.log( this.a );
 };
}
var obj1 = {
 a: 2
};
var obj2 = {
 a: 3
}
var bar = foo.call( obj1 );
bar.call( obj2 ); // 23.でない!

アロー関数は、イベントハンドラやタイマーなどのコールバック関数でよく使われます:


function foo() { 
 setTimeout(() => {
 // ここでの thisは、このようにfoo()を継承している。
 console.log( this.a ); },100);
 }
var obj = { 
	a:2
};
foo.call( obj ); // 2

ES6は、従来の thisメカニズムの代わりにレキシカル・スコープを使用し、アロー関数と同様のパターンを持っています。

function foo() {
 var self = this; // lexical capture of this
 setTimeout( function() {
 console.log( self.a ); // selfは、foo()関数の thisバインディングを継承しているだけである。
 }, 100 );
}
var obj = {
 a: 2
};
foo.call(obj); // 2

call と apply の実装

call と apply の理解

callメソッドとapplyメソッドは基本的に同じことをします。関数が実行されるコンテキストを変更するために作成され、その本当の力は、関数が動作するスコープを拡張することです。平たく言えば、関数本体内のthis を変更するということです。

栗をひとつ:

window.color = "red";
var o = {color: "blue"};
function sayColor(){
	alert(this.color);
}
sayColor();//red
sayColor.call(this);//red関数sayColorのボディ内の thisは現在の環境にバインドされている。
sayColor.call(window);//red関数sayColorのボディ内のthisはウィンドウにバインドされている。
sayColor.call(o);//blue

説明:上記の栗は、それは関数sayColorがグローバルスコープで呼び出され、グローバルスコープ内のcolor属性があることは明らかであり、値は "赤 "であり、sayColor.call(this)コードのこの行は、関数本体sayColor内部""this""、現在の環境にバインドされていることを示すことです。""") この行は、関数本体 sayColor 内部の this を現在の環境に結びつけることを意味し、sayColor.call(window)この行は関数本体sayColor内部のthisをwindowにバインドすることを意味しています。最後に、sayColor.call(o)の行は関数sayColor内部のthisをoにバインドすることを意味します。「つまりsayColor内部の "this"-->oです。

callと apply

call() と apply() の違いは、 call() メソッドが複数の引数のリストを受け付けるのに対し、 apply() メソッドは複数の引数の配列を受け付けることです

一例です:

var func = function(arg1, arg2) {
 ...
};
func.call(this, arg1, arg2); // callを使うと、引数リスト
func.apply(this, [arg1, arg2]) // 引数の配列でapplyを使う

アプリケーションのシナリオ

2つの配列のマージ

var a = ['a', 'aa'];
var b = ['b', 'bb'];
Array.prototype.push.apply(a, b);
console.log(a) // ['a', 'aa', 'b', 'bb']

実は今、代わりにconcatがあるんです。あるいは他の変なトリック。でも、そんなことはどうでもいいんです。

関数の引数の数には制限があります。JSコアは65535までで、例外をスローするエンジンもあれば、例外をスローせずに余分な引数を失うエンジンもあります。

では、解決策は?

配列を分割することは可能です。そして、それを一括して呼び出します。

function myConcat(arr1, arr2, max = 32768) {
 for(var i = 0; i < arr2.length; i += max) {
 Array.prototype.push.apply(
 	arr1,
 arr2.slice(i, i + max) // マイナー・ポイント、第2引数が配列の長さより大きい場合は、配列の終わりまで取る。
 )
 }
}
var a = [-2, -1];
var b = [];
for(var i = 0; i < ; i ++) {
 b.push(i)
}
Array.prototype.push.apply(a, b) // Uncaught RangeError: Maximum call stack size exceeded
myConcat(a, b)

配列であることを確認

var arr = [];
Object.prototype.toString.call(arr); // [object Array]
//関数本体にオブジェクト.prototype.toString()メソッド内の thisは、arrの実行環境に結びついている。

arr.toString()の結果が、同じオブジェクト型の検出でObject.prototype.toString.call(arr)の結果と異なります。

これは、toString()は、Objectのプロトタイプメソッドであり、配列、関数などの参照型は、toStringメソッドをオーバーライドObjectのインスタンスであるためです。異なるオブジェクトの種類は、プロトタイプチェーンの知識によると、toStringメソッドの後に対応する書き換えを呼び出すtoStringメソッドを呼び出すと、ObjectのtoStringメソッドのプロトタイプを呼び出すために行くことはありませんので、arr.toString()の使用は、オブジェクトの型を取得することはできません、唯一の文字列の型に変換arrすることができます。したがって、オブジェクトの特定の型を取得したい場合は、ObjectのプロトタイプのtoStringメソッドを呼び出す必要があります。

バインドについて

var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function
var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1"); // 505 環境によって異なるデータ
// (505) ["h1", html.gr__hujiang_com, head, meta, ...] 

配列のようなオブジェクトには、以下の2つの特性があります。

  • 1, with: object 要素への添え字の数値と length 属性。
  • 2、持っていません:例えば、push、shift、forEach、indexOf配列オブジェクトはメソッドを持っています。

push/pop/shift/unshift Array.prototype.slice.call クラス配列オブジェクトはオブジェクトです。JSには引数オブジェクトのようなクラス配列という名前のオブジェクト構造が存在し、DOM APIが返すNodeListオブジェクトはクラス配列オブジェクトに属します。

クラスの配列オブジェクトを配列に変換するその他のメソッド:

// 上記のコードは
var arr = [].slice.call(arguments) 
ES6:
let arr = Array.from(arguments);
let arr = [...arguments];

Array.from()は、2種類のオブジェクトを真の配列に変換することができます。

呼び出しの実装

ここでは暗黙のバインディングを使用します。

最初にテストする例を捨ててください:

var a = 1;
function f () {
 console.log(this.a)
}
var b = {
 a: 2
}
f(); // 1
f.call(b) // 2

暗黙のバインディングを適用すると、直接バインドできるようになります。


var a = 1;
function f () {
 console.log(this.a)
}
Function.prototype.myCall = function (context) {
 context.fn = f;
 context.fn();
 delete context.fn;
}
var b = {
 a: 2
}
f(); // 1
f.myCall(b) // 2

非常に巧妙な方法です。しかし、パラメータを受け入れることはできないようです。パラメータを受け入れるには、undefinedやnullなどの境界ケースを考慮する必要があります。と!そして!callが渡されないと、デフォルトでwindowが適用されます;

そうしてきたし、これからもそうするでしょうパンチ

Function.prototype.myCall = function(context) {
 // 渡されたオブジェクトを取得する。例えば、上記のfooオブジェクトのように、contextは上記のfooと等価である。
 // 最初のパラメータがない場合、デフォルトはwindowである。,
 var context = context || window;
 // コンテキストに属性を追加する。この場合、thisは、上記のbar関数のように、myCallを呼び出す関数を指す。
 context.fn = this;//ここでコンテキスト.fnこれは上記のbar関数と等価である。
 // コンテキストの後のパラメータは、展開演算子や分解代入によって取り出される。上の例では、パラメータのリストを渡していない。
 var args = [...arguments].slice(1);
 // 関数の実行
 var result = context.fn(...args);
 // 関数の削除
 delete context.fn;
 return result;
};

applyの実装

彼らは2つの兄弟であるため、パラメータは同じではありませんので、説明しないで、直接コードを見てください。

Function.prototype.myApply = function(context) {
 var context = context || window;
 context.fn = this;
 var result;
 // 第2パラメータが存在するかどうか、つまりコンテキストの後に配列があるかどうかを判断する。
 // 存在する場合、2番目のパラメータは展開される
 if (arguments[1]) {
 result = context.fn(...arguments[1]);
 } else {
 result = context.fn();
 }
 delete context.fn;
 return result;
}

バインドの実装

バインドの理解

bind()メソッドは新しい関数を作成し、bind()が呼び出されたときに、新しい関数の thisがbind()`の最初の引数として指定され、残りの引数は新しい関数が呼び出されたときの引数として使用されます。

文法:bind() メソッドは、オブジェクトを返す新しい関数を作成する。

MDN

バインドの実装

bindメソッドとcall/applyメソッドの最大の違いは、前者がコンテキストにバインドされた関数を返すのに対し、後者の2つは関数を直接実行することです。

アップコード

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}

上記のコードから、バインドには以下のような特徴があることがわかります:

  • this "を指定できます。
  • 関数を返します。
  • コンストラクタはパラメータとして渡すことができます。
  • 未定義

トップレベル

仕事に直行!

Function.prototype.myBind = function (context) {
 var that = this; // ここで使われているクロージャは、現時点で thisを保存している。そうでないと、後で呼び出されたときに暗黙のうちに失われてしまうからだ。
 return function () { 
 return that.apply(context)
 }
}
// テストケース
var value = 2;
var foo = {
 value: 1
};
function bar() {
	return this.value;
}
var bindFoo = bar.myBind(foo);
console.log(bindFoo()); // 1

では、パラメータとコリエリゼーションについて説明します。

Function.prototype.myBind = function (context) {
 // パラメータはsliceメソッドを使用しないクラスの配列なので、呼び出しによって thisをバインドした後、配列をコピーするためにsliceを使用する。ただし、最初にバインドする必要がある thisは後述する。
 var args = Array.prototype.slice.call(arguments, 1)
 var that = this; // ここで使われているクロージャは、現時点で thisを保存している。そうでないと、後で呼び出されたときに暗黙のうちに失われてしまうからだ。
 return function () { 
 // 上記のようにパラメータをコピーする
 var nowArgs = Array.prototype.slice.call(arguments)
 return that.apply(context, args.concat(nowArgs))
 }
}
// テストケース
var value = 2;
var foo = {
 value: 1
};
function bar(name, age) {
 return {
		value: this.value,
		name: name,
		age: age
 }
};
var bindFoo = bar.myBind(foo, "Jack");
bindFoo(20);
// {value: 1, name: "Jack", age: 20}

バインドには次のような特徴があります。

バインドされた関数は、new演算子を使ってオブジェクトを生成することもできます。これは、元の関数をコンストラクタとして扱うようなもので、与えられた this値は無視され、呼び出しの引数はモック関数に与えられます。

例:通常のバインド

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となっています。これはグローバル値でもfooオブジェクト内の値でもありません。つまり、バインドのthisオブジェクトは失敗しています。thisオブジェクトが失敗したので、thisの新しい実装が新しいオブジェクトを生成します。thisはobjを指しています。

そのため、returnするときに、コンストラクタとして返されるかどうかを判断する必要があります。コンストラクタとして返される場合は、現在のthisを返し、そうでない場合は、現在の入力コンテキストをバインドします。

Function.prototype.myBind = function (context) {
 // パラメータはsliceメソッドを使用しないクラスの配列なので、呼び出しによって thisをバインドした後、配列をコピーするためにsliceを使用する。ただし、最初にバインドする必要がある thisは後述する。
 var args = Array.prototype.slice.call(arguments, 1)
 var that = this; // ここで使われているクロージャは、現時点で thisを保存している。そうでないと、後で呼び出されたときに暗黙のうちに失われてしまうからだ。
 var fnc = function () { 
 // 上記のようにパラメータをコピーする
 var nowArgs = Array.prototype.slice.call(arguments)
 //コンストラクタとして使われる場合、thisはインスタンスを指す。この場合、this instanceof fBoundはtrueとなり、インスタンスはバウンド関数から値を得ることができる。属性を持つことになる。
//通常の関数として使われる場合、thisはウィンドウを指し、この場合結果はfalseとなり、バインドされた関数の thisはコンテキストを指す。
 return that.apply(
 this instanceof fnc ? this : context, 
 args.concat(nowArgs)
 )
 }
 // 返された関数のプロトタイプをバインドされた関数のプロトタイプに変更することで、インスタンスはバインドされた関数のプロトタイプの値を継承することができる。
 fnc.prototype = this.prototype
 return fnc
}

しかし、実はこれには問題があります。インスタンスがプロトタイプを変更した場合、次の継承が問題になります。

今回は、プロトタイプの上のパラメータをコピーする必要があります。これは

Douglas Crockford氏は2006年にPrototypal Inheritance in JavaScriptという記事を書いています。この記事では、厳密な意味でのコンストラクタを使わない継承の実装方法について説明しています。このアイデアは、プロトタイプを使って既存のオブジェクトを基に新しいオブジェクトを作成することで、結果としてカスタム型を作成する必要がなくなるというものです。これを実現するために、彼は次のような関数を用意しています。

function object(o){
 function F(){}
 F.prototype = o
 return new F
}

とてもシンプルに見えます。

まず、オブジェクト関数内に一時コンストラクタFを作成し、次に入力オブジェクトoを一時コンストラクタのプロトタイプとして使用し、最後に一時コンストラクタのインスタンスを返します。

簡単に言うと、objectは入力オブジェクトの浅いコピーを作成します。

新しいオブジェクトを生成するには、ES5のObject.create()メソッドを使用します。

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

ただし、bindとObject.create()はES5メソッドであり、一部のIEブラウザではサポートされていないため、Polyfillでbindを実装するためにObject.create()を使用することはできませんが、原理は同じです。

じゃあ、変えましょう。

Function.prototype.myBind = function (context) {
 // パラメータはsliceメソッドを使用しないクラスの配列なので、呼び出しによって thisをバインドした後、配列をコピーするためにsliceを使用する。
 var args = Array.prototype.slice.call(arguments, 1)
 var that = this; // ここで使われているクロージャは、現時点で thisを保存している。そうでないと、後で呼び出されたときに暗黙のうちに失われてしまうからだ。
 var fNOP = function () {};
 var fnc = function () { 
 // 上記のようにパラメータをコピーする
 var nowArgs = Array.prototype.slice.call(arguments)
 //コンストラクタとして使われる場合、thisはインスタンスを指す。この場合、this instanceof fBoundはtrueとなり、インスタンスはバウンド関数から値を得ることができる。属性を持つことになる。
//通常の関数として使われる場合、thisはウィンドウを指し、この場合結果はfalseとなり、バインドされた関数の thisはコンテキストを指す。
 return that.apply(
 this instanceof fNOP ? this : context, 
 args.concat(nowArgs)
 )
 }
 // 返された関数のプロトタイプをバインドされた関数のプロトタイプに変更することで、インスタンスはバインドされた関数のプロトタイプの値を継承することができる。
 fNOP.prototype = this.prototype
 fnc.prototype = new FNOP()
 return fnc
}

ここで、実はもう少しで解決しそうなのですが、ふと、バインド呼び出しが関数ではないという別の問題があることを思い出し、今回は例外をスローする必要があります。

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

わけ

newの理解

Function.prototype.myBind = function (context) {
 if (typeof this !== "function") {
 	throw new Error("Function.prototype.bind - what is trying to 	be bound is not callable");
	}
 // パラメータはsliceメソッドを使用しないクラスの配列なので、呼び出しによって thisをバインドした後、配列をコピーするためにsliceを使用する。ただし、最初にバインドする必要がある thisは後述する。
 var args = Array.prototype.slice.call(arguments, 1)
 var that = this; // ここで使われているクロージャは、現時点で thisを保存している。そうでないと、後で呼び出されたときに暗黙のうちに失われてしまうからだ。
 var fNOP = function () {};
 var fnc = function () { 
 // 上記のようにパラメータをコピーする
 var nowArgs = Array.prototype.slice.call(arguments)
 //コンストラクタとして使われる場合、thisはインスタンスを指す。この場合、this instanceof fBoundはtrueとなり、インスタンスはバウンド関数から値を得ることができる。属性を持つことになる。
//通常の関数として使われる場合、thisはウィンドウを指し、この場合結果はfalseとなり、バインドされた関数の thisはコンテキストを指す。
 return that.apply(
 this instanceof fNOP ? this : context, 
 args.concat(nowArgs)
 )
 }
 // 返された関数のプロトタイプをバインドされた関数のプロトタイプに変更することで、インスタンスはバインドされた関数のプロトタイプの値を継承することができる。
 fNOP.prototype = this.prototype
 fnc.prototype = new FNOP()
 return fnc
}

new の実装

new の実装

new 演算子は、ユーザー定義オブジェクト型のインスタンスか、コンストラクタを持つ組み込みオブジェクトのインスタンスを作成します。 --

まず、現在利用可能な新しいものを試してみて、どのような特徴を持っているのか、そして要約した後、実装を開始します。

見て:

function NEW (a) {
 this.a = a;
}
NEW.prototype.sayA = function () {
 console.log(this.a+'b')
}
var A = new NEW(1);
A.sayA()
A.a

AはNEWのインスタンスであることがわかります。これはコンストラクタのプロパティとプロトタイプのプロパティを継承しています。

新しい キーワードは次のようなものです:

  1. 空のシンプルな JavaScript オブジェクトを作成します;
  2. このオブジェクトを別のオブジェクトにリンクします;
  3. ステップ1で新しく作成したオブジェクトをthisのコンテキストとして使用します;
  4. 関数がオブジェクトを返さない場合は、thisを返します。

MDN

newをつくる

言って、実行するだけです:

// new はキーワードであり、直接オーバーライドすることはできない。ここでは、newの効果をシミュレートするためにcreateを使っている。
function create () {
 // 1.
 var obj = new Object();
 
 // コンストラクタを取得する
 // 引数は、クラスの配列オブジェクトであるため、unshiftメソッドがないので、呼び出しの使用は、newこのキーワードはなく、唯一のシミュレーションは、彼がコンストラクタを渡す必要がある引数の1つ。
 var Con = [].unshift.call(arguments)
 
 // 2.
 obj.__proto__ = Con.prototype 
 
 // 3this "にバインドされている。引数はすでに最初の要素から取り除かれているので、残りの引数は必須である。
 Con.apply(obj, arguments)
 
 // 4.  
 return obj
 
}

コンストラクタの戻り値には、以下の3つのケースがあります:

  • はオブジェクトを返します。
  • 戻り値なし、つまり未定義を返します。
  • 3、undefined以外の基本型を返します。
function create() {
	// 1コンストラクタは、引数から第1引数を取り除くことで得られる。
 Con = [].shift.call(arguments);
	// 2objはコンストラクタ・プロトタイプのプロパティにアクセスできる。
 var obj = Object.create(Con.prototype);
	// 3this "をobjにバインドすることで継承が実装され、objはコンストラクタ内のプロパティにアクセスできる。
 var ret = Con.apply(obj, arguments);
	// 4コンストラクタが返したオブジェクトを優先的に返す。
	return ret instanceof Object ? ret : obj;
};
Read next

React antdのオンデマンドロードとカスタムテーマ

ここで使っている方法は、npm run ejectメソッドでwebpackの設定を公開して自分で修正する方法です。

Feb 28, 2020 · 3 min read