1、为什么要用this
function identify() { return this.name.toUpperCase(); } function speak() { var greeting = "Hello, I'm " + identify.call( this ); console.log( greeting ); } var me = { name: "Kyle" }; var you = { name: "Reader" }; identify.call( me ); identify.call( you ); speak.call( me ); speak.call( you);
如果不是要this,需要给函数显式传入上下文对象。
function identify(context) { return context.name.toUpperCase(); } function speak(context) { var greeting = "Hello, I'm " + identify.call( context ); console.log( greeting ); } var me = { name: "Kyle" }; var you = { name: "Reader" }; identify( me ); speak( you );
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简介并且易于复用。
2、误解
2.1 指向自身
//记录函数foo被调用的次数 function foo(num) { console.log( "foo: " + num); // 记录foo被调用次数 this.count++; } foo.count = 0; var i; for (i = 0; i < 10; i++) { if(i > 5){ foo( i ); } } console.log( foo.count ); // foo: 6 // foo: 7 // foo: 8 // foo: 9 //0
console.log产生了4条输出,证明foo()被调用4次,但foo.count是0。this并非指向自身。
function foo() { foo.count = 4; //foo指向它自身 } setTImeout(function() [ //匿名函数无法指向自身 }, 100)
arguments.callee用于引用当前正在运行的函数对象。这是唯一可以从匿名函数对象内部引用自身的方法。但更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。
强制this指向foo函数对象
//记录函数foo被调用的次数 function foo(num) { console.log( "foo: " + num); // 记录foo被调用次数 this.count++; } foo.count = 0; var i; for (i = 0; i < 10; i++) { if(i > 5){ //使用call( )可以确保this指向函数对象foo自身 foo.call(foo, i); } } console.log( foo.count ); // foo: 6 // foo: 7 // foo: 8 // foo: 9 //4
2.2 它的作用域
第二章常见的误解是this指向函数的作用域,在某些情况下它是正确的,但其他情况下是错误的。
this在任何情况下都不指向函数的词法作用域。在JavaScript内,作用域和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部。
3、this是什么
this是在运行时绑定的,不是在编写时绑定的,它的上下文取决于函数调用的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
4、调用位置
调用位置就是函数在代码被调用的位置。某些编程模式会隐藏真正的调用位置。最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数)。
我们要关心的调用位置就在当前正在执行的函数的前一个调用中。
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的调用位置
5、绑定规则
找到绑定位置,判断需要应用下面四条规则中的哪一条。
5.1 默认绑定
//独立函数调用 //无法应用其他规则时的默认规则 function foo( ) { console.log( this.a ); } var a = 2; foo(); // 2
foo( )直接使用不带任何修饰的函数引用进行调用,因此只能使用默认绑定,因此this指向全局对象。
如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到undefined。
function foo( ) { "use strict" console.log( this.a ); } var a = 2; foo(); //TypeError: this is undefind
虽然this的绑定完全取决于调用位置,但只有foo( )运行在非strict mode下,默认绑定才能绑定到全局对象,在严格模式下调用foo( )不影响默认绑定。
function foo( ) { console.log( this.a ); } var a = 2; (function(){ "use strict" foo(); // 2 }())
5.2 隐式绑定
另一条要考虑的规则是调用位置是否有上下文对象, 或者说是否被某个对象拥有或包含,不过这种说法可能会造成些误导。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo } obj.foo(); // 2
无论是直接在obj中定义还是先定义再添加为引用属性,foo严格来说都不属于obj对象。
调用位置会使用obj上下文来引用函数,可以说函数被调用时obj对象“拥有”或“包含”它。当函数引用有上下文对象时,隐式绑定规则会把函数调用的this绑定到这个上下文对象。
对象属性引用链只有上一层或者说最后一层在调用 位置起作用:
function foo() { console.log( this.a ); } var obj = { a: 2, obj2: obj2 } var obj2 = { a: 42, foo: foo } obj.obj2.foo(); // 42
隐式丢失
虽然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"; bar(); // "oops, global"
参数传递是一种隐式赋值,因此传入函数时也会被隐式赋值。
function foo() { console.log( this.a ); } function doFoo(fn) { fn() // <-- 调用位置 } var obj = { a: 2, foo: foo } var a = "oops, global"; doFoo( obj.foo ); // "oops, global"
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo } var a = "oops, global"; setTimeout( obj.foo, 100); // "oops, global" //JavaScript 环境中内置的setTimeout()函数实现和下面的伪代码类似: function setTimeout(fn, delay) { // 等待delay毫秒 fn(); // <-- 调用位置 }
5.3 显式绑定
call和apply方法第一个参数是对象,在调用时函数将其绑定到this,称之为显式绑定。
function foo(){ console.log( this.a ); } var obj = { a:2 } foo.call( obj ); // 2
如果传入了一个原始值(字符串类型、布尔类型或数字类型)来作为this的绑定对象,这个原始值会被转出成它的对象类型(new String(...)、new Boolean(...)或new Number(...))。这通常被成为“装箱”
5.3.1硬绑定
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
5.3.2API调用的“上下文”
第三方库的许多函数,以及JavaScript和宿主焊接的许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(...)一样,确保回调函数使用指定的this。
这些函数实际上通过call或apply实现了显式绑定。
function foo(el) { console.log(el, this.id); } var obj = { id: "awesome" }; // 调用foo()时把this绑定到obj [1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
5.4 new绑定
在JavaScript,构造函数只是用new操作符时被调用的函数,不会属于某个类,也不会实例化一个类。 包含内置对象函数(如Number())在内的所有函数都可以用new调用。这种函数调用被称为构造函数调用。这里有一个重要但细微的区别:实际上并不存在所谓的构造函数,只有对于函数的构造调用。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,new表达式的函数调用会自动返回这个新对象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new调用foo(),会构造一个新对象并把它绑定到foo()调用的this上。
6 优先级
默认绑定的优先级是最低的。
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; } obj1 = { foo: foo; } 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绑定比隐式绑定优先级高
new和apply/call无法一起使用,因此无法通过new foo.call(obj1)来测试,但可以通过硬绑定测试。
function foo(something) { this. a = something; } 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
new bar( 3 )没有把obj1.a修改为3,new修改了硬绑定(到obj1的)调用bar()中的this。因为使用了new绑定,得到一个名为baz的新对象,且baz.a值为3。
在new中使用硬绑定主要是为了预先设置函数的一些参数,这样使用new进行初始化时就可以只传入其余的参数。bind()的功能之一是把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层函数(这种技术成为“部分应用”,是“柯里化”的一种)。
判断this
1.函数是否在new中调用(new绑定)?如果是this绑定的是新创建的对象。
var bar = new foo();
2.函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是,this绑定的是指定的对象。
var bar = foo.call(obj2);
3.函数是否在摸个上下文对象中调用(隐式绑定)?如果是,this绑定到哪个上下文对象。
var bar = obj1.foo();
4.如果都不少,使用默认绑定。如果在严格模式下,绑定到undefined,否则绑定到全局对象。
var bar = foo();
7 绑定例外
在某些情况下,你认为应用其他绑定规则时,实际上应用了默认绑定规则。
7.1被忽略的this
如果把nul或undefined作为this的绑定对象传入call、apply或bind,会应用默认绑定规则。
一种常用的做法是使用apply展开一个数组并作为参数传入一个函数,bind可以对参数进行柯里化(预先设置一些参数):
function foo(a, b) { console.log("a: " + a + ",b:" + b); } foo.apply( null, [2, 3]); var bar = foo.bind( null, 2); bar( 3 );
总是使用null来忽略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]); var bar = foo.bind( ∅, 2); bar( 3 );
7.2间接绑定
你可能有意或无意创建一个函数的”间接引用“ ,这时调用函数会应用默认绑定。
间接引用最容易在赋值时发生:
// 间接引用 function foo(a) { 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绑定对象的不少调用位置是否处于严格模式,而是函数体是否处于严格模式。
7.3软绑定
硬绑定会降低函数的灵活性,使用硬绑定后就无法使用隐式绑定或显式绑定来修改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; }; } 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 应用了软绑定
8 tihs词法
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
箭头函数可以像bind确保函数的this被绑定到指定的对象。
在ES6之前已经在使用一种几乎和箭头函数完全一样的模式。
function foo() { var self = this; setTimeout( function() { console.log( self.a ); }, 100) } var obj = { a:2 }; foo.call(obj); // 2