これは、JavaScript の最も興味深い機能と言えます。受動態、過去形、現在形、過去進行形など、高校英語のさまざまな時制と同じように、何度見逃しても、次回も間違えるかもしれません。この記事は、「あなたの知らない JavaScript 第 1 巻」に触発され、これを JavaScript で要約したものです。
これを学ぶための最初のステップは、これが関数自体を指しているわけでも、関数のスコープを指しているわけでもないことを理解することです。これは実際には関数が呼び出されたときに発生するバインディングであり、それが指す場所は関数が呼び出される場所に完全に依存します。
デフォルトバインディング
JavaScript では、最も一般的に使用されるタイプの関数呼び出しは独立した関数呼び出しであるため、このルールは、他のルールを適用できない場合のデフォルトのルールと見なすことができます。関数が変更せずに呼び出された場合、つまり「ほとんど」呼び出された場合、デフォルトのバインディング ルールが適用され、デフォルトのバインディングはグローバル スコープを指します。
function sayLocation() { console.log(this.atWhere) } var atWhere = "I am in global" sayLocation() // 默认绑定,this绑定在全局对象,输出 “I am in global”
別の例を見てください
var name = "global" function person() { console.log(this.name) // (1) "global" person.name = 'inside' function sayName() { console.log(this.name) // (2) "global" 不是 "inside" } sayName() // 在person函数内部执行sayName函数,this指向的同样是全局的对象 } person()
この例では、 person 関数がグローバル スコープで呼び出されるため、文 (1) の this はグローバル オブジェクト (ブラウザーのウィンドウ、ノードではグローバル) にバインドされます。したがって、文 (1) の自然な出力は、グローバル オブジェクトの name 属性です。これはもちろん「グローバル」です。たとえ person 関数が name 属性を設定したとしても、sayName 関数は person 関数内で呼び出されます。
これはデフォルトのバインド ルールであり、JavaScript で最も一般的な関数呼び出しパターンであり、4 つのバインド ルールの中で最も単純であり、グローバル スコープにバインドされます。
デフォルトバインディングのストリクトモード
JavaScriptではストリクトモードを使用するとグローバルオブジェクトにバインドできません。まだ最初の例を使用していますが、今回は厳密モード宣言を追加しています
'use strict' function sayLocation() { console.log(this.atWhere) } var atWhere = "I am in global" sayLocation() // Uncaught TypeError: Cannot read property 'atWhere' of undefined
厳密モードでは、これがグローバル オブジェクトにバインドされている場合、実際には未定義にバインドされていることがわかります。そのため、上記のコードは、エラー。
暗黙的バインディング
関数が呼び出されるとき、関数にいわゆる「足場」、つまりコンテキスト オブジェクトがある場合、暗黙的バインディング ルールによって関数内のこれがコンテキスト オブジェクトにバインドされます。上の段落が十分に分かりにくいと思われる場合は、コードを見てみましょう。
そうですとても簡単ですね。上記のコードでは、obj1 と obj2 は、いわゆる Say 関数の開始点です。より専門的な用語は、コンテキスト オブジェクトが関数に割り当てられると、関数内の this は当然コンテキスト オブジェクトを指します。これは非常に一般的な関数呼び出しパターンでもあります。
暗黙的なバインディング中にコンテキストが失われます
function say() { console.log(this.name) } var obj1 = { name: "zxt", say: say } var obj2 = { name: "zxt1", say: say } obj1.say() // zxt obj2.say() // zxt
ここでの出力が「グローバル」であることがわかります。なぜ上記の例と違うのでしょうか?明らかに obj.say の名前を変更しただけです。
まず見てください。上記の文 (1) のコード。JavaScript では関数はオブジェクトであるため、オブジェクトは値ではなく参照によって渡されます。したがって、文 (1) のコードは alias = obj.say =say になります。つまり、alias = Say は、最終的には Say 関数のアドレスを参照するだけであり、何も行いません。オブジェクト obj を使用します。これを「コンテキストの喪失」といいます。最後にalias関数が実行されますが、say関数は単純に実行され「global」が出力されます。
明示的バインディング
明示的バインディングは、その名前が示すように、これをコンテキストに明示的にバインドします。JavaScript では、apply、call、bind の 3 つの明示的バインディング メソッドが提供されます。 apply と call の使い方は基本的に似ています:
apply(obj,[arg1,arg2,arg3,...] 呼び出される関数のパラメータは配列
call の形式で与えられます。 (obj,arg1 , arg2, arg3,...) 呼び出された関数のパラメーターは順番に与えられます
、bind 関数が実行された後、新しい関数が返されます
function say() { console.log(this.name) } var name = "global" var obj = { name: "inside", say: say } var alias = obj.say // 设置一个简写 (1) alias() // 函数调用 输出"global" (2)
したがって、apply の役割がわかります。 , call は、実行コンテキストを関数にバインドするものであり、関数内の this は、当然、call または apply によって呼び出されるオブジェクトにバインドされます。
バインド関数は、指定された実行コンテキストにバインドされた新しい関数を返します
// 不带参数 function speak() { console.log(this.name) } var name = "global" var obj1 = { name: 'obj1' } var obj2 = { name: 'obj2' } speak() // global 等价于speak.call(window) speak.call(window) speak.call(obj1) // obj1 speak.call(obj2) // obj2
つまり、バインドメソッドは新しい関数を返すだけで、この関数は実行コンテキストを指定します。返される新しい関数はパラメーターを受け入れることができます。
新しいバインディング
最後に説明する this バインディング ルールは、 new 演算子を介してコンストラクターが呼び出されたときに発生する this バインディングを指します。 JavaScript には他の言語のようなクラスという概念はありません。コンストラクターは単なる関数ですが、コンストラクターの関数名は大文字で始まり、単に new 関数を呼び出すことができます。
function Person(name,age) { this.name = name this.age = age console.log("我也只不过是个普通函数") } Person("zxt",22) // "我也只不过是个普通函数" console.log(name) // "zxt" console.log(age) // 22 var zxt = new Person("zxt",22) // "我也只不过是个普通函数" console.log(zxt.name) // "zxt" console.log(zxt.age) // 22
上面这个例子中,首先定义了一个 Person 函数,既可以普通调用,也可以以构造函数的形式的调用。当普通调用时,则按照正常的函数执行,输出一个字符串。 如果是通过一个new操作符,则构造了一个新的对象。那么,接下来我们再看看两种调用方式, this 分别绑定在了何处首先普通调用时,前面已经介绍过,此时应用默认绑定规则,this绑定在了全局对象上,此时全局对象上会分别增加name 和 age 两个属性。当通过new操作符调用时,函数会返回一个对象,从输出结果上来看 this 对象绑定在了这个返回的对象上。
因此,所谓的new绑定是指通过new操作符来调用函数时,会产生一个新对象,并且会把构造函数内的this绑定到这个对象上。
事实上,在javascript中,使用new来调用函数,会自动执行下面的操作。
1.创建一个全新的对象
2.这个新对象会被执行原型连接
3.这个新对象会绑定到函数调用的this
4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
四种绑定的优先级
上面讲述了javascript中四种this绑定规则,这四种绑定规则基本上涵盖了所有函数调用情况。但是如果同时应用了这四种规则中的两种甚至更多,又该是怎么样的一个情况,或者说这四种绑定的优先级顺序又是怎么样的。
首先,很容易理解,默认绑定的优先级是最低的。这是因为只有在无法应用其他this绑定规则的情况下,才会调用默认绑定。那隐式绑定和显式绑定呢?还是上代码吧,代码可从来不会说谎。
function speak() { console.log(this.name) } var obj1 = { name: 'obj1', speak: speak } var obj2 = { name: 'obj2' } obj1.speak() // obj1 (1) obj1.speak.call(obj2) // obj2 (2)
所以在上面代码中,执行了obj1.speak(),speak函数内部的this指向了obj1,因此(1)处代码输出的当然就是obj1,但是当显式绑定了speak函数内的this到obj2上,输出结果就变成了obj2,所有从这个结果可以看出显式绑定的优先级是要高于隐式绑定的。事实上我们可以这么理解obj1.speak.call(obj2)这行代码,obj1.speak只是间接获得了speak函数的引用,这就有点像前面所说的隐式绑定丢失了上下文。好,既然显式绑定的优先级要高于隐式绑定,那么接下来再来比较一下new 绑定和显式绑定。
function foo(something) { this.a = something } var obj1 = {} var bar = foo.bind(obj1) // 返回一个新函数bar,这个新函数内的this指向了obj1 (1) bar(2) // this绑定在了Obj1上,所以obj1.a === 2 console.log(obj1.a) var baz = new bar(3) // 调用new 操作符后,bar函数的this指向了返回的新实例baz (2) console.log(obj1.a) console.log(baz.a)
我们可以看到,在(1)处,bar函数内部的this原本指向的是obj1,但是在(2)处,由于经过了new操作符调用,bar函数内部的this却重新指向了返回的实例,这就可以说明new 绑定的优先级是要高于显式绑定的。
至此,四种绑定规则的优先级排序就已经得出了,分别是
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数中的this绑定
箭头函数是ES6里一个重要的特性。
箭头函数的this是根据外层的(函数或者全局)作用域来决定的。函数体内的this对象指的是定义时所在的对象,而不是之前介绍的调用时绑定的对象。举一个例子
var a = 1 var foo = () => { console.log(this.a) // 定义在全局对象中,因此this绑定在全局作用域 } var obj = { a: 2 } foo() // 1 ,在全局对象中调用 foo.call(obj) // 1,显示绑定,由obj对象来调用,但根本不影响结果
从上面这个例子看出,箭头函数的 this 强制性的绑定在了箭头函数定义时所在的作用域,而且无法通过显示绑定,如apply,call方法来修改。在来看下面这个例子
// 定义一个构造函数 function Person(name,age) { this.name = name this.age = age this.speak = function (){ console.log(this.name) // 普通函数(非箭头函数),this绑定在调用时的作用域 } this.bornYear = () => { // 本文写于2016年,因此new Date().getFullYear()得到的是2016 // 箭头函数,this绑定在实例内部 console.log(new Date().getFullYear() - this.age) } } } var zxt = new Person("zxt",22) zxt.speak() // "zxt" zxt.bornYear() // 1994 // 到这里应该大家应该都没什么问题 var xiaoMing = { name: "xiaoming", age: 18 // 小明永远18岁 } zxt.speak.call(xiaoMing) // "xiaoming" this绑定的是xiaoMing这个对象 zxt.bornYear.call(xiaoMing) // 1994 而不是 1998,这是因为this永远绑定的是zxt这个实例
因此 ES6 的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this ,具体来说就是,箭头函数会继承 外层函数调用的this绑定 ,而无论外层函数的this绑定到哪里。
小结
以上就是javascript中所有this绑定的情况,在es6之前,前面所说的四种绑定规则可以涵盖任何的函数调用情况,es6标准实施以后,对于函数的扩展新增了箭头函数,与之前不同的是,箭头函数的作用域位于箭头函数定义时所在的作用域。
而对于之前的四种绑定规则来说,掌握每种规则的调用条件就能很好的理解this到底是绑定在了哪个作用域。