this
关于this
为什么用this
使用this可以隐式传递一个对象引用,因此可以将API设计的更加简洁并易于复用
//使用this
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = "Hello,I'm a" + identify.call(this);
console.log(greeting);
}
var me = {
name: "people";
}
var you = {
name: 'pig';
}
identify.call(me); //PEOPLE
identify.call(you); //PIG
speak.call(me); //Hello,I'm a PEOPLE;
speak.call(you); //Hello,I'm a PIG;
//不使用this,就只能通过显示传入一个上下文对象
function identify(context){
return context.name.toUpperCase();
}
function speak(context){
var greeting = "Hello,I'm a" + identify(context);
}
this到底是啥
this是在运行时进行绑定的,并不是在编写时绑定,他的上下文取决于函数调用时的各种条件
this的绑定和函数声明位置没有任何关系,只取决于函数的调用方法
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文),这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this就是这个记录的一个属性,会在函数执行过程中用到
对this的误解
指向自身
这里我们来看一个例子:
function foo(num){
console.log("foo:"+num);
this.count++; //this不是指向那个函数对象
}
foo.count = 0; //向函数对象添加了一个属性count
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
为什么上述的代码片段中,this行为与预期不一样?
很明显,这里的this并不是指向foo本身,其实this指向的是全局window
如果要从函数对象内部引用它自身,那只使用this是不够的,一般来说需要通过一个指向函数对象的词法标识符来引用他
对比:
function foo(){
foo.count = 4; //具名函数可以指向自身
}
setTimeout(function(){
//匿名函数无法指向自身
},10);
注:有一种方法可以让匿名函数指向自身arguments.callee,但是这种方法现在已经弃用了,因为访问arguments是一个昂贵的操作,他是一个很大的对象,每次递归调用都需要重新创建,影响浏览器性能,还会影响闭包
解决上述问题的方法:
-
使用词法作用域
-
强制
this指向foo函数对象function foo(num){ console.log("foo:"+num); this.count++; } foo.count = 0; var i; for(i=0; i<10; i++){ if(i>5){ foo.call(foo, i); //确保this指向函数对象foo本身 } } console.log(foo.count); //输出预期结果
this的作用域
this在任何情况下都不指向函数的词法作用域
作用域无法通过JS代码访问,它存在于JS引擎内部
function foo(){
var a = 2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo(); // undefined
解析:
- 这段代码试图使用
this.bar()去调用bar(),一般直接调用bar() - 试图使用
this连接foo和bar的词法作用域
绑定规则
函数在执行过程中是如何决定this的绑定对象的呢?
默认绑定
最常用的函数调用类型:独立函数调用
这个规则可以看成是无法应用其他规则时的默认规则
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2,函数调用时应用了this的默认绑定,所以指向全局对象
foo直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法使用其他规则
注意:
-
严格模式环境下,不能将全局对象用于默认绑定,所以**
this会绑定到undefined**function fn(){ 'use strict'; console.log(this) //undefined console.log(this.a); } var a = 2; fn() //报错 -
函数以及调用都暴露在严格模式时,
this会绑定到undefined'use strict'; var a = 2; function fn(){ console.log(this) //undefined console.log(this.a); //报错 } fn(); -
严格模式下调用不在严格模式中的函数,不会影响
this指向var a = 2; function fn(){ console.log(this); //window console.log(this.a); //2 } (function(){ 'use strict'; fn(); })();
隐式绑定
判断调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含
-
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的
this绑定到这个上下文对象,也就是说,如果函数调用时,前面存在调用它的对象,那么this就会绑定到这个对象上function foo(){ console.log(this.a); } var obj = { a: 2, foo: foo } obj.foo(); //2 -
对象属性引用链只有上一层或者最后一层在调用位置中起作用,也就是说,如果函数调用前存在多个对象,
this指向距离调用自己最近的对象function foo(){ console.log(this.a); } var obj2 = { a: 20, foo: foo } var obj1 = { a: 40, obj2: obj2 } obj1.obj2.foo(); // 20注:但是如果
obj2没有a,那么最终就会输出undefined,作用域链跟原型链并不一样
隐式丢失
this绑定有个问题:被隐式绑定的函数会丢失绑定对象,也就是说他会应用默认绑定
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo; //函数别名,是obj.foo的一个引用
//但是,实际上他引用的是foo本身,所以此时bar是一个不带任何修饰的函数调用,所以也就应用了默认绑定
var a = 'global'; //全局对象上的属性
bar(); //'global'
还有一种情况(传入回调函数):
function foo(){
console.log(this.a);
}
function doFoo(fn){ //函数传递实际上是一种隐式赋值
fn(); //所以此处的fn实际上是foo本身
}
var obj = {
a: 2,
foo: foo
}
var a = 'global';
doFoo(obj.foo); //'global'
并且,把函数传入内置函数而不是自己声明的函数,也会出现回调函数丢失this绑定的情况
注:关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this
如果嵌套函数作为方法调用,其
this的值指向调用它的对象如果嵌套函数作为函数调用,其
this值不是全局对象(非严格模式下)就是undefined(严格模式下)
注:并不是所有的隐式绑定丢失都指向全局对象
var a = 2;
let obj = {
a: 3,
fn: function(){
console.log(this.a);
}
}
let obj2 = {
a: 4
}
obj2.fn = obj.fn; //此处虽然丢失了obj的隐式绑定,但是在赋值过程中,又建立了新的隐式绑定,指向obj2
obj2.fn(); // 4
显式绑定
在隐式绑定中,我们必须在一个对象的内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上
但是现在想要不在对象内部包含函数引用,而在某个对象上强制调用函数,怎么做?
JS提供了两个函数:call()和apply():
第一个参数是一个对象,是给
this准备的,接着在调用函数时将其绑定到this因为可以直接指定
this的绑定对象,因此称为显式绑定
function foo(){
console.log(this.a);
}
var obj = {
a: 2
}
foo.call(obj); // 2,此处将foo的this强制绑定到obj上了
但是如果使用call()和apply()的时候,传入一个原始值(字符串,布尔值,数字)来当作this的绑定对象,这个原始值会被转换成它的对象形式(new String()、new Boolean()、new Number()),这就被称为装箱
如果call()和apply()指向null或者undefined,那么this将指向全局对象
但是显示绑定并不能解决丢失绑定的问题
硬绑定
function foo(){
console.log(this.a);
}
var obj = {
a: 2
}
var bar = function(){
foo.call(obj); //在bar内部手动调用了foo.call(obj),强制把foo的this绑定到obj
//所以无论如何调用函数bar,都总会手动在obj上调用foo
}
bar(); //2
setTimeout(bar, 100); // 2
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 -
创建一个可以重复使用的辅助函数
funciton 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); //会返回一个新函数,会将指定的参数设置为this的上下文并调用原始函数
var b = bar(3); //2 3
console.log(b); //5
API调用的上下文
第三方库许多函数,以及JS语言和宿主环境中有许多新的内置函数,都提供了一个可选的参数,一般称为上下文(context),作用和bind一样
function foo(el){
console.log(el, this.id);
}
var obj = {
id: 'aaa'
}
[1, 2, 3].forEach(foo, obj); //1 aaa 2 aaa 3 aaa
注:call、apply与bind有什么区别
- call、apply在改变this指向的同时还会执行函数,而bind在改变this后是返回一个全新的boundFcuntion绑定函数
- bind属于硬绑定,返回的 boundFunction 的 this 指向无法再次通过bind、apply或 call 修改;call与apply的绑定只适用当前调用,调用完就没了,下次要用还得再次绑
- call与apply功能完全相同,唯一不同的是call方法传递函数调用形参是以散列形式,而apply方法的形参是一个数组。在传参的情况下,call的性能要高于apply,因为apply在执行时还要多一步解析数组
new绑定
JS中的构造函数,在JS中,构造函数只是一些使用new操作符时被调用的函数,并不会属于某个类,也不会实例化一个类
实际上他们并不能说是一种特殊类型的函数,他们只是被new操作符调用的普通函数而已
包括内置对象函数在内的所有函数都可以用new进行调用,这种函数调用被称为构造函数调用,但是有一个区别:实际上并不存在所谓的构造函数,只有对于函数的构造调用
使用new调用函数:
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
this.a = a;
}
var bar = new foo(2); //会构造一个新对象并把它绑定到foo()调用中的this上
console.log(bar.a); // 2
this绑定优先级
默认绑定的优先级最低
隐式绑定和显式绑定
显示绑定优先级要大于隐式绑定
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绑定
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
var bar = new obj1.foo(4);
console.log(obj1.a); //2
console.log(bar.a); //4
new绑定和显示绑定
new绑定优先级要大于显示绑定
function foo(something){
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1); //bar被硬绑到obj1
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2,此处没有把obj1.a改成3
console.log(baz.a); // 3,而是修改了baz新对象
结论:
- 函数是否在有
new中调用,是的话this绑定的是新创建的对象- 函数是否通过
call,apply显示绑定,是的话this绑定的是指定的对象- 函数是否在上下文对象中调用,是的话绑定那个上下文对象
- 如果都不是就使用默认绑定,严格模式下绑定到
undefined,否则绑定到全局对象
绑定例外
被忽略的this
如果把null、undefined作为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际上应用的是默认绑定规则
但是什么情况下会传入null呢? 看看apply的应用场景:
apply可以来展开一个数组,并当作参数传入一个函数
function foo(a, b){
console.log("a:" + a + ", b:" + b )
}
foo.apply(null, [2, 3]); //a:2, b:3
此处null作为一个占位符,因为不关心this的指向
这样可能会产生一些不可预计的后果,所以现在有另外一种方案让this更安全
创建一个DMZ(非军事区)对象,作为一个空的非委托的对象
任何的this使用都可以限制在这个空对象中,不会对全局对象产生任何影响
创建方法:Object.create(null) ,这种方式和直接{}很像,但是它不会创建Object.prototype,比{}更空
function foo(a, b){
console.log("a:" + a + ", b:" + b )
}
var DMZ = Object.create(null);
foo.apply(DMZ, [2, 3]); //a:2, b:3
间接引用
间接引用最容易发生在赋值的时候,应用默认绑定
下面的赋值表达式p.foo = o.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,调用的是foo本身,所以应用默认绑定
软绑定
硬绑定可以把this强制绑定到指定的对象,防止函数调用应用默认绑定规则,但是硬绑定的灵活性很低,使用硬绑定就不能使用隐式绑定和显式绑定来修改this了
软绑定让 this在默认情况下不再指向全局对象(非严格模式)或**undefined(严格模式),而是指向两者之外的一个对象(这点和硬绑定的效果相同),但是同时又保留了隐式绑定和显式绑定在之后可以修改this指向的能力**
所以就有一种绑定称为软绑定:Function.prototype.softBind
除了软绑定外,softBind其他原理和bind类似
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
箭头函数
之前的规则可以应用于所有正常的函数,但是ES6中有一种无法使用这些规则的特殊函数:箭头函数
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
function foo(){
return a=>{
console.log(this.a);
}
}
var obj1 = {a: 2}
var obj2 = {a: 3}
var bar = foo.call(obj1)
bar.call(obj2); // 2,不是3
箭头函数最常用于回调函数中,可以像bind一样保证函数的this被绑定到指定对象
在ES6之前,就有一种替代方案:
function foo(){
var self = this;
setTimeout(function(){
console.log(self.a);
}, 100);
}
var obj = {
a: 2
}
foo.call(obj); // 2