函数中的this是一个刚开始接触比较头疼的概念,总感觉它变来变去,害怕一不留神它就不是原来的它了。
ES6用了箭头函数干脆把this固定住,不允许它变,但是在更复杂的场景中,我们恰恰需要this的多变性,所以,还是必须把this的绑定规则搞定。
1、this对象
this是JavaScript中一个很特别的关键字,被自动定义在所有函数的作用域中。在函数被调用的时候,this才具有指向性。this引用的是函数执行时的环境对象,也就是函数执行时的作用域对象。不是函数声明时的作用域对象。
1.1、调用位置
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
最重要的是要分析调用栈,就是为了到达当前执行位置所调用的所有函数,我们关心的调用位置就在当前正在执行的函数的前一个调用中。
// 调用栈就是当前正在执行的函数 // 调用位置就在当前正在执行的函数的前一个调用中。 function baz(){ //当前调用栈是:baz // 因此,当前调用位置是全局作用域 console.log('baz'); bar(); //bar的调用位置 console.log('baz'); } function bar(){ // 当前调用栈是baz->bar // 因此,当前调用位置在baz中。 console.log('bar'); foo();//foo的调用位置 console.log('bar'); } function foo(){ //调试命令,可以让程序停留在这里 debugger; // 当前调用栈是baz->bar->foo // 因此,当前调用位置在bar中。 console.log('foo'); } baz(); //baz的调用位置
1.2、绑定规则
1.2.1、默认绑定
最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其它规则时的默认规则。
function foo(){ console.log(this.a); } //声明在全局作用域中的变量就是全局对象的一个属性。 var a = 2; // 函数被调用时应用了this的默认绑定,this指向全局对象window foo(); // 2
1.2.2、隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } obj.foo(); // 2
函数foo()被当做引用属性添加到obj中,但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。
然而,调用位置会使用obj上下文来引用函数,因为可以说函数被调用时obj对象“拥有”或者包含”它。
当foo()被调用时,它的前面加上了对obj的引用。当函数引用有上下文对象时,隐式绑定规则会把函数调用时的this绑定到这个上下文对象。
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo } var obj1 = { a:3, obj2:obj, } obj1.obj2.foo();//2
1.2.3、隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined中。取决于是否是严格模式。(非严格模式绑定到全局对象,严格模式绑定到undefined中)
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo, } //bar是obj.foo的一个引用,但是实际上,它引用的是函数foo本身。函数引用的上下文丢失 var bar = obj.foo; // 函数别名! var a = '全局属性的值'; //此时bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。 bar(); // 全局属性的值
一种更常见的情况发生在传入回调函数时:
function foo(){ console.log(this.a); } //传入回调函数 function doFoo(fn){ //fn其实引用的是foo fn(); //foo的调用位置 } var obj = { a:2, foo:foo } var a = '全局属性的值'; //传递参数其实也是一种隐式赋值,传入函数时也会被隐式赋值。 // fn = obj.foo 函数别名 doFoo(obj.foo);//全局属性的值
把函数传入语言内置的函数,情况也是一样的。
function foo(){ console.log(this.a); } var obj = { a : 2, foo : foo } var a = "全局属性的值"; setTimeout(obj.foo,1000); //全局属性的值 //和下面的代码原理相似: //fn = obj.foo 函数别名 function setTimeout(fn,delay){ //等待delay毫秒 fn(); //调用位置 }
回调函数丢失this绑定是非常常见的。那么如何固定this呢?
1.2.4、显式绑定
隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。
如果不想在对象内部包含函数引用,只想在某个对象上强制调用函数,该怎么做呢?
函数作为对象,也拥有自己的方法,call()和apply()方法,JavaScript提供的函数和自己创建的所有函数都可以使用call()和apply()方法。
方法的第一个参数是一个对象,是给this准备的,在调用函数时将其绑定到this。因为可以直接指定this的绑定对象,这种方法称之为显式绑定。
function foo(){ console.log(this.a); } var obj = { a:2 } foo.call(obj); // 2
通过foo.call(),可以在调用foo()函数时强制把foo里面的this绑定到obj上。
如果传入了一个原始值(字符串类型、布尔值类型、数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(new String()、new Boolean()、new Number()),这通常被称为“装箱”。
1、硬绑定
function foo() { console.log(this.a); } var obj = { a: 2 } //显式的强制绑定叫做硬绑定 var bar = function () { foo.call(obj); } bar(); // 2 setTimeout(bar, 100); // 2 var a = '全局'; //硬绑定的bar不可能再修改它的this bar.call(window); // 2
硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:
function foo(something){ console.log(this.a,something); return this.a + something; } var obj = { a:2 } var bar = function(){ //把bar的arguments参数传递给foo,apply支持传递arguments参数。 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 } //利用JavaScript提供的bind函数直接硬绑定 var bar = foo.bind(obj); var b = bar(3); // 2 3 console.log(b);//5 console.log(bar === foo); // false
bind()会返回一个新函数,它会把你指定的参数设置为this的上下文并调用原始函数。
2、API调用的“上下文”
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文(context)”,其作用和bind()一样,确保你的回调函数使用指定的this。
function foo(el){ console.log(el,this.id); } var obj = { id:'myid' } var id = '全局'; //调用foo时把this绑定到obj [1,2,3].forEach(foo,obj);
这些函数实际上就是通过call()等实现了显示绑定,可以少写一些代码实现this的绑定。
1.2.5、new绑定
这是最后一条this的绑定规则。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
1、创建一个全新的对象。
2、这个新对象会被执行[[prototype]]连接。
3、这个新对象会绑定到函数调用的this。
4、如果函数没有返回其它对象,那么new表达式中的函数调用会自动返回这个新对象。
function Foo(a){ this.a = a ; } //使用new调用foo函数,会构造出一个新对象,并把这个新对象绑定到foo函数调用中的this上。 var bar = new Foo(2); console.log(bar.a); // 2
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,它们并不是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
所以,所有函数都可以用new来调用,这种函数调用被称为构造函数调用,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
1.3、优先级
默认绑定的优先级是四条规则中最低的。
显示绑定的优先级高于隐式绑定。
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
new绑定比隐式绑定优先级高
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 //new绑定比隐式绑定的优先级高 var bar = new obj1.foo(4); console.log(bar.a); // 4 console.log(obj1.a);//2
new绑定的优先级高于显示绑定
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
现在可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
可以按照下面的顺序进行判断:
1、函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new Foo()
2、函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
3、函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj.foo()
4、如果都不是的话,使用默认绑定。如果是在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()
1.4、绑定例外
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定。
function foo(){ console.log(this.a); } var a = 2; foo.call(null); // 2
什么情况下会传入null呢?
一种常用的做法是用apply()来展开一个数组,并当做参数传入一个函数。
function foo(a,b){ console.log(a + b); } // 把数组展开成参数 foo.apply(null,[2,3]); // 5 //使用bind()进行柯里化 var bar = foo.bind(null,2); bar(3); // 5
这两种方法都需要传入一个参数当做this的绑定对象,如果函数不关心this的话,仍然需要传入一个占位值,null非常方便。
let arr = [12,45,14,38]; let max = Math.max.apply(null,arr); console.log(max); // 45
然而,总是使用null来忽略this绑定可能产生一些副作用,如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定会把this绑定到全局对象window上,这可能会导致不可预计的后果(比如修改全局对象)。所以,使用null会导致难以分析和追踪的bug。
更安全的this
一种更安全的方法是传入一个特殊的对象,把this绑定到这个对象不会对程序产生任何副作用。
通过创建一个空对象,就不会对全局对象产生任何影响。
// 创建一个空对象,这个空对象比{}更空,因为并不会创建Object.prototype对象。 var φ = Object.create(null); // console.log(φ,{}) function foo(a,b){ console.log(a + b); } // 把数组展开成参数 foo.apply(φ,[2,3]); // 5 //使用bind()进行柯里化 var bar = foo.bind(φ,2); bar(3); // 5
2、this词法
在ES6中有一种无法使用上面四种规则的特殊函数类型:箭头函数。
箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
function foo(){ //返回一个箭头函数 return a => { //this继承自foo() console.log(this.a); }; /* return function(a){ console.log(this.a); }; */ } let obj1 = { a:2 }; let obj2 = { a:3 }; let bar = foo.call(obj1); bar.call(obj2);// 2
foo()内部创建的箭头函数会捕获调用时foo()的 this,由于foo()的this绑定到了obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo(){ setTimeout(()=>{ //这里的this在词法上继承自foo() console.log(this.a); },1000) } let obj1 = { a:2 }; foo.call(obj1); // 2
箭头函数可以像bind()一样确保函数的this被绑定到指定对象上,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。
在ES6之前,有一种几乎和箭头函数完全一样的模式:
function foo(){ //把this对象保存在变量中 var _this = this; setTimeout(function(){ //通过_this变量来使用保存好的this对象 console.log(_this.a); },1000) } let obj1 = { a:2 }; foo.call(obj1); // 2
3、构造函数
其实,在JavaScript中,构造函数并不是一种特殊的函数,只是受到Java等面向对象,拥有类概念的语言的影响。
构造函数首字母要大写这种规矩也是来源于对Java等语言的追随。
function Foo(){ return this; } let o = new Foo(); console.dir(Foo);//prototype是函数的一个属性,可以叫做原型对象。 //函数的prototype对象里面有一个constructor属性,又指向函数本身。 console.log(Foo.prototype.constructor === Foo); // true //o是一个实例,它有一个原型链[[Prototype]]属性,指向new出自己的函数的原型对象。 console.log(o); //通过__proto__可以访问实例的原型对象。 console.log(o.__proto__); //o实例并没有constructor属性,但是通过对原型链的往上层访问,可以访问到原型对象里面的constructor属性。 console.log(o.constructor === Foo);// true
实际上,Foo函数与其他函数没有任何区别。函数本身不是构造函数,当在普通函数调用前面加上new关键字后,就会把这个函数调用变成一个“构造函数调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。
function nothingSpecial(){ console.log('我是一个普通函数'); } let obj = new nothingSpecial(); console.log(obj); // {}
nothingSpecial()只是一个普通函数,使用new调用的时候,就会产生一个对象并赋值给obj,使用new调用一个函数无论如何都会返回一个对象。这个new的调用是一个构造函数的调用,但是nothingSpecial()却不是一个 构造函数。
可以理解为在JavaScript中,对于“构造函数”最准确的解释是:所有带new的函数调用。
函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
当函数作为构造函数调用的时候,函数内部的this指向new返回出来的对象。
发表评论:
◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。