当一个函数被调用时,会建立一个执行上下文。这个执行上下文会包含函数在哪里被调用(执行栈)、函数的调用方式、传入的参数等信息。this就是这个执行上下文的一个属性,会在函数执行的过程当中用到。javascript
在理解this
的绑定过程以前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。java
咱们关心的调用位置就在当前正在执行的函数的前一个调用中。数组
function baz() { // 当前调用栈是:baz // 所以,当前调用位置是全局做用域 console.log( "baz" ); bar(); // <-- bar的调用位置 } function bar() { // 当前调用栈是:baz --> bar // 所以,当前调用位置在baz中 console.log( "bar" ); foo(); // <-- foo的调用位置 } function foo() { // 当前调用栈是:baz --> bar --> foo // 所以,当前调用位置在bar中 console.log( "foo" ); } baz(); // <-- baz的调用位置
this
的绑定规则总共有5种:安全
new
绑定独立函数调用 :能够把这条规则看做是没法应用其余规则时的默认规则。闭包
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
严格模式(strict mode),不能将全局对象用于默认绑定,this会绑定到undefined
。app
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: Cannot read property 'a' of undefined
当函数引用有上下文对象时,隐式绑定规则会把函数中的this
绑定到这个上下文对象。函数
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
对象属性引用链中只有上一层或者说最后一层在调用位置中起做用。oop
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1={ a:2, obj2:obj2 } obj1.obj2.foo() // 42
隐式丢失post
被隐式绑定的函数特定状况下会丢失绑定对象,它会应用默认绑定,把this
绑定到全局对象或者undefined
上(取决因而否是严格模式)。this
// 虽然bar是obj.foo的一个引用,可是实际上,它引用的是foo函数自己。 // 所以此时的bar()是一个不带任何修饰的函数调用,所以应用了默认绑定。 function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函数别名! var a = "oops, global"; // a是全局对象的属性 bar(); // "oops, global"
一种更微妙、更常见而且更出乎意料的状况发生在传入回调函数时:
function foo() { console.log( this.a ); } function doFoo(fn){ //fn其实引用的是foo fn();// <-- 调用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局对象的属性 doFoo(obj.foo); // "oops, global"
参数传递其实就是一种隐式赋值,咱们传入函数时也会被隐式赋值。
若是把函数传入语言内置的函数而不是传入本身声明的函数,结果是同样的。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局对象的属性 setTimeout(obj.foo,100); // "oops, global" // JS环境中内置的setTimeout()函数实现和下面的伪代码相似: function setTimeout(fn, delay) { // 等待delay毫秒 fn(); // <-- 调用位置! }
可使用函数的call(...)
和apply(...)
方法,它们的第一个参数是一个对象,是给this
准备的,接着在调用函数时将其绑定到this
。由于你能够直接指定this
的绑定对象,所以咱们称之为显式绑定。
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2
经过foo.call(...)
咱们能够在调用foo
时强制把它的this
绑定到obj
上。
惋惜,显式绑定仍然没法解决咱们以前提出的丢失绑定的问题。
硬绑定
可是显式绑定的一个变种能够解决这个问题。
function foo() { console.log( this.a ); } var obj = { a: 2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬绑定的bar不可能再修改它的this bar.call( window ); // 2
咱们建立了函数bar()
,并在它的内部手动调用了foo.call(obj)
,所以强制把foo
的this
绑定到了obj
。
不管以后如何调用函数bar
,它总会手动在obj
上调用foo
。咱们称之为硬绑定。
典型应用场景是建立一个包裹函数,负责接收参数并返回值:
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
ES5提供了内置的方法Function.prototype.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
bind(...)
会返回一个硬编码的新函数,它会把你指定的参数设置为this
的上下文并调用原始函数。
API调用的“上下文”
第三方库以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,一般被称为“上下文”(context),其做用和bind(...)
同样,确保你的回调函数使用指定的this
。
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" } let myArr = [1,2,3] // 调用foo(..)时把this绑定到obj myArr.forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
这些函数实际上就是经过call(...)
或者apply(...)
实现了显式绑定。
在Javascript中,构造函数只是一些使用new
操做符时被调用的函数。它们并不属于某个类,也不会实例化一个类。
包括内置对象函数(好比Number(...)
)在内的全部函数均可以用new
来调用,这种函数调用被称为构造函数调用。
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操做。
this
。new
表达式中的函数调用会自动返回这个新对象。function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new
来调用foo(...)
时,咱们会构造一个新对象并把它绑定到foo(...)
调用中的this
上。
new
是又一种能够影响函数调用时this
绑定行为的方法,咱们称之为new
绑定。
function foo1() { console.log(this.a) } function foo2(something) { this.a = something } var obj1 = { a: 2, foo: foo1 } var obj2 = { a: 3, foo: foo1 } var obj3 = { foo: foo2 } var obj4 = {} obj1.foo(); //2 obj2.foo(); //3 obj1.foo.call(obj2); //3 obj2.foo.call(obj1); //2 //可见,显式绑定比隐式绑定优先级高 obj3.foo(4); console.log(obj3.a); //4 obj3.foo.call(obj4, 5); console.log(obj4.a); //5 var bar = new obj3.foo(6); console.log(obj3.a); //4 console.log(bar.a); //6 //可见,new绑定比隐式绑定优先级高 var qux = foo2.bind(obj4); qux(7); console.log(obj4.a); //7 var quux = new qux(8); console.log(obj4.a); //7 console.log(quux.a); //8 //new绑定修改了硬绑定(到obj4的)调用qux(...)中的this。
如今,咱们能够根据优先级来判断函数在某个调用位置应用的是哪条规则。
new
中调用(new
绑定),若是是的话this
绑定的是新建立的对象。call
、apply
(显式绑定)或者硬绑定调用,若是是的话this
绑定的是指定的对象。this
绑定的就是那个上下文对象。若是都不是的话,使用默认绑定。
undefined
。被忽略的this
若是你把null
或者undefined
做为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认规则。
function foo(){ console.log(this.a); } var a = 2; foo.call(null); //2
两种状况会传入null
apply(...)
来“展开”一个数组,并当作参数传入一个函数。bind(...)
能够对参数进行柯里化(预先设置一些参数)。function foo(a, b) { console.log( "a:" + a + ",b:" + b ); } // 把数组”展开“成参数 foo.apply( null, [2, 3] ); // a:2,b:3 // 使用bind(..)进行柯里化 var bar = foo.bind( null, 2 ); bar( 3 ); // a:2,b:3
老是使用null
来忽略this
绑定可能会产生一些反作用。
若是某个函数确实使用了this
(比方说第三方库中的一个函数),那默认绑定规则会把this
绑定到全局对象,这将致使不可预计的后果(好比修改全局对象)。
更安全的this
一种“更安全”得作法是传入一个特殊的对象,把this
绑定到这个对象不会对你的程序产生任何反作用。
在Javascript中建立一个空对象最简单的方法是Object.create(null)。它和{}
很想,可是并不会建立Object.prototype
这个委托。
function foo(a, b) { console.log( "a:" + a + ",b:" + b ); } // 咱们的空对象 var ø = Object.create( null ); // 把数组”展开“成参数 foo.apply( ø, [2, 3] ); // a:2,b:3 // 使用bind(..)进行柯里化 var bar = foo.bind( ø, 2 ); bar( 3 ); // a:2,b:3
另外一个须要注意的是,你有可能建立一个函数的“间接引用”,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值时发生:
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
赋值表达式p.foo = o.foo
的返回值是目标函数的引用,所以调用位置是foo()
而不是p.foo()
或者o.foo()
。
硬绑定这种方式能够把this
强制绑定到指定的对象(除了使用new
时),防止函数调用应用默认绑定规则。
缺点是硬绑定会大大下降函数的灵活性,使用硬绑定以后就没法使用隐式绑定或者显式绑定来修改this
。
若是能够给默认绑定指定一个全局对象和undefined之外的值,那就能够实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定来修改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; }; }
除了软绑定以外,softBind(...)
的其它原理和ES5内置的bind(...)
相似。
它会对指定的函数进行封装,首先检查调用时的this
,若是this
绑定到全局对象或者undefined
,那就把指定的默认对象obj
绑定到this
,不然不会修改this
。
function foo() { console.log("name:" + this.name); } var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; 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
能够看到,软绑定版本的foo()
能够手动将this
绑定到obj2
或者obj3
上,但若是应用默认绑定,则会将this
绑定到obj
。
咱们以前介绍的四条规则能够包含全部正常的函数。可是ES6
中介绍了一种没法使用这些规则的特殊函数类型:箭头函数。
箭头函数不使用this
的四种标准规则,而是根据外层(函数或者全局)做用域来决定this
。
function foo() { //返回一个箭头函数 return (a) => { //this继承自foo() console.log(this.a); }; } var obj1 = { a:2 }; var obj2 = { a:3 }; var bar =foo.call(obj1); bar.call(obj2); //2,不是3!
foo()
内部建立的箭头函数会捕获调用时的foo()
的this
。因为foo()
的this
绑定到obj1
,bar
(引用箭头函数)的this
也会绑定到obj1
,箭头函数的绑定没法被修该。(new
也不行!)
箭头函数经常使用于回调函数中,例如事件处理器或者定时器:
function foo(){ setTimeout(()=>{ //这里的this在词法上继承自foo() console.log(this.a); },100); } var obj = { a:2 }; foo.call(obj) //2
1.箭头函数没有prototype
(原型),因此箭头函数自己没有this
。
let a = () => {} console.log(a.prototype) //undefined
2.箭头函数中的this
是从定义它们的上下文继承的(Javascript权威指南第7版P206),继承自外层第一个普通函数的this
。
let foo let barObj = { msg: 'bar的this指向' } let bazObj = { msg: 'baz的this指向' } bar.call(barObj) //bar的this指向barObj baz.call(bazObj) //baz的this指向bazObj function bar() { foo = () => { console.log(this, 'this指向定义它们的上下文,外层的第一个普通函数') } } function baz() { foo() } //msg: "bar的this指向" "this指向定义它们的上下文,外层的第一个普通函数"
3.箭头函数的this
没法经过bind
,call
,apply
来直接修改。
let quxObj = { msg: '尝试直接修改箭头函数的this指向' } function baz() { foo.call(quxObj) } //{msg: "bar的this指向"} "this指向定义它们的上下文,外层的第一个普通函数"
间接修改箭头函数的指向:
bar.call(bazObj) //普通函数bar的this指向bazObj,内部的箭头函数也会指向bazObj
被继承的普通函数的this
指向改变,箭头函数的this
指向也会跟着改变。
4.若是箭头函数没有外层函数,this
指向window
var obj = { i: 10, b: () => console.log(this.i, this), c: function() { console.log( this.i, this) } } obj.b()//undefined, window obj.c()//10, {i: 10, b: ƒ, c: ƒ}
/** * Question 1 * 非严格模式下 */ var name = 'window' var person1 = { name: 'person1', show1: function () { console.log(this.name) }, show2: () => console.log(this.name), show3: function () { return function () { console.log(this.name) } }, show4: function () { return () => console.log(this.name) } } var person2 = { name: 'person2' } person1.show1() person1.show1.call(person2) person1.show2() person1.show2.call(person2) person1.show3()() person1.show3().call(person2) person1.show3.call(person2)() person1.show4()() person1.show4().call(person2) person1.show4.call(person2)()
正确答案以下:
person1.show1() // person1,隐式绑定 person1.show1.call(person2) // person2,显式绑定 person1.show2() // window,箭头函数绑定,没有外层函数,指向window person1.show2.call(person2) // window,箭头函数绑定,不能直接修改,仍是指向window person1.show3()() // window,高阶函数,person1.show3()返回一个函数ƒ(){ console.log(this.name)}到全局 //从而致使最终函数执行环境是window,因此此时this 指向 var name = 'window' person1.show3().call(person2) // person2 ,返回函数之后,显式绑定person2,this指向person2对象 person1.show3.call(person2)() // window,高阶函数ƒ(){return function(){console.log(this.name)}} 显式绑定person2, //也就是高阶函数this指向person2,它的返回值ƒ(){console.log(this.name)}执行环境是window,同上 person1.show4()() // person1,箭头函数绑定,this是从定义函数的上下文继承的,也就是外层函数所在的上下文,外层函数的this指向person1 person1.show4().call(person2) // person1,没法经过call直接修改箭头函数绑定 person1.show4.call(person2)() // person2,高阶函数,外层函数this显式绑定person2,修改箭头函数的外层函数this指向,能够改变箭头函数this指向
/** * Question 2 */ var name = 'window' function Person (name) { this.name = name; this.show1 = function () { console.log(this.name) } this.show2 = () => console.log(this.name) this.show3 = function () { return function () { console.log(this.name) } } this.show4 = function () { return () => console.log(this.name) } } var personA = new Person('personA') var personB = new Person('personB') personA.show1() personA.show1.call(personB) personA.show2() personA.show2.call(personB) personA.show3()() personA.show3().call(personB) personA.show3.call(personB)() personA.show4()() personA.show4().call(personB) personA.show4.call(personB)()
正确答案以下:
personA.show1() // personA,new绑定之后,构造函数Person中的this绑定到personA,Person传入的参数personA,因此结果是personA personA.show1.call(personB) // personB,new绑定之后,构造函数Person中的this绑定到personA,personA.show1就是ƒ(){console.log(this.name)} // 再显式绑定personB,personA.show1的this指向personB实例对象,因此结果是personB personA.show2() // personA,new绑定之后,构造函数Person中的this绑定到personA,personA.show2就是()=>console.log(this.name) //而后箭头函数绑定,调用箭头函数,this指向外层函数的this.name,也就是personA personA.show2.call(personB) // personA,new绑定之后,构造函数Person中的this绑定到personA,personA.show2就是()=>console.log(this.name) //箭头函数不能直接修改,因此仍是personA personA.show3()() // window,new绑定之后,构造函数Person中的this绑定到personA,personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局 // 执行环境是window,因此执行之后的结果是var name = 'window',也就是window personA.show3().call(personB) // personB,new绑定之后,构造函数Person中的this绑定到personA, // personA.show3()返回一个函数ƒ(){console.log(this.name)}到全局, // 再显式绑定personB,因此最终结果是personB personA.show3.call(personB)() // window,new绑定之后,构造函数Person中的this绑定到personA, //高阶函数ƒ(){return function(){console.log(this.name)}}显式绑定personB, //返回一个函数ƒ(){console.log(this.name)}到全局 //执行环境是window,因此执行之后的结果是var name = 'window',也就是window personA.show4()() // personA,new绑定之后,构造函数Person中的this绑定到personA, // 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name),执行箭头函数 // 箭头函数绑定,继承外层普通函数this,因此结果是personA personA.show4().call(personB) // personA,new绑定之后,构造函数Person中的this绑定到personA, // 高阶函数ƒ(){return ()=>console.log(this.name)}执行后返回箭头函数()=>console.log(this.name), // 箭头函数不能直接修改,因此结果仍是personA personA.show4.call(personB)() // personB,new绑定之后,构造函数Person中的this绑定到personA, // 显式绑定 外层函数 ,因此箭头函数也被修改成 personB
[你不知道的JavaScript 上卷]
[Javascript权威指南第七版]