目录
关于this
比如,我们在同一个方法内使用了一个外部变量a和this,此时,不管该方法被谁调用,这个外部变量a指向的永远都是该方法定义时所在代码块外部的那个变量a(这就是词法作用域)不会改变,但是this不一样,它会因为调用该方法的对象的改变而改变!
其实简单来说,正常情况下,this绑定的是当前方法的调用对象,注意不是定义时的词法作用域,它与词法作用域最大的区别就是:随着调用该方法的对象的不同,this指向的对象也会改变!
为什么要用this
如果说词法作用域是方法与定义时的代码块内外交流的通道,那么this就是方法与被调用对象间的唯一关联,通过词法作用域,我们可以读取使用外部变量,通过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); // KYLE
identify.call(you); // READER
speak.call(me); // Hello, 我是KYLE
speak.call(you); // Hello, 我是READER
所以this这个磨人的小妖精,魔力就在于能更优雅的给开发者上下文环境
this的误解
太拘泥于“this”的字面意思就会产生一些误解。有两种常见的对于this的解释,但是它们都是错误的。
指向自身
function foo(num) {
console.log(num);
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 5; i++) {
foo(i);
}
console.log('count=' + foo.count); // ->>> 0
console.log('globalCount=' + window.count); // ->>> NaN
行之后我们发现在foo函数中分别打印了0,1,2,3,4,按我们“预期”的结果,应该要打印出count = 5,结果遗憾的是0。 事实上当执行foo.count = 0时,函数对象确实被添加了一个count属性,但是函数中的this.count跟这个count并不是同一个。其实是创建了一个全局变量count,值为NaN。
当前执行的方法是window对象下的,执行过程this指向window,而count的自加变成了 undefined + 1,而undefined 在做隐式类型转换会变成NaN,所以值为NaN
误解二:this指向函数的作用域
function foo() {
var a = 2;
this.bar()
}
function bar() {
console.log(this.a)
}
foo(); // ->>> undefined
此处视图使用this.a去访问foo的a,但是无法将作用域连接起来。此处this还无法引用作用域内部的东西。
this 和词法作用域是两个不同的、没有交叉的概念,有各自的用途。具体而言,词法作用域是在编写代码时就确定的,this ,是在运行时确定的。运行时想使用某个对象,可以修改 this。
this全面解析
调用位置
寻找调用位置就是寻找“函数被调用的位置”,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。
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的调用位置
在运行代码的时候,CPU计数器会记录当前执行函数的地址及函数返回后要跳转的地址。函数调用会使用call指令,在将函数的入口地址设定到程序计数器之前,call指令会把调用函数后要执行的指令地址存储在主存内的栈中。函数处理完毕后,再通过函数的出口来执行return命令。return命令的功能是把保存在栈中的地址设定到程序计数器中。
绑定规则
我们来看看在函数的执行过程中调用位置如何决定this的绑定对象。 四种绑定规则:
- 1.默认绑定(函数被声明的时候默认绑定)
- 2.隐式绑定(对象引用了函数的时候会隐式绑定到对象上)
- 3.显示绑定(调用call、apply、bind)
- 4.new操作符绑定(new执行四个动作)
———————————————扫盲专区——————————
bind,apply,call的区别
- bind,apply,call都会改变this的指向,区别在于:
- 1. bind会返回一个改变指向后的函数,不会立即执行;
- 2. apply,call会立即执行该函数;
- 3. apply第二个参数传的是个数组(也可以是arguments);
- 4. call是从第二个参数开始依次传入。
new在构造函数中做了哪些事
- //1.在内存中创建一个新的对象
- //2.让this指向这个新对象
- //3.在构造函数里面的代码,给这个新对象添加属性和方法
- //4.返回这个新对象 所以构造函数里面一般不需要return
① 默认绑定
this默认绑定为window,我们在全局作用域下调用了foo,找不到指向,默认就是window。需要注意的是:如果开启严格模式,this.name打印的将是undefined。但是严格模式与foo调用的位置无关,还是默认绑定到Window
var name = 'a';
function foo() {
// "use strict";
var name = "b";
console.log(this.name); //a
console.log(this); //Window
}
foo();
this在非严格模式下,默认指向全局;在严格模式下,默认为undefined;在调用得情况下,谁调用就指向谁。
② 隐式绑定
判断调用的位置是否有上下文对象
var name = 'a'
function foo() {
console.log(this.name);
}
var obj = {
name: 'b',
foo: foo
}
obj.foo() // b
foo() // a
foo函数本质是不属于obj对象的,而是在obj中作为引用属性。但是当通过obj间接调用foo时,this的指向就发生了变化,此时this就绑定到了obj。如果是直接调用,毋庸置疑,this指向的是全局的。
函数不属于某一个对象,只是说函数被调用时,某个对象引用了它。
如果有多层的引用,那么this指向的就是最近的一层,毕竟远水救不
function foo() {
console.log(this.name);
}
var obj = {
name: 'b',
foo: foo
}
var obj2 = {
name: 'c',
obj: obj
}
obj2.obj.foo() // b
❤❤❤❤❤❤❤-------------隐式丢失-------------❤❤❤❤❤❤
但是也会出现一种情况,间接引用的时候,很可能会被绑定到全局中
var name = "a";
function foo() {
console.log(this.name);
}
var obj = {
name: "b",
foo: foo
};
var bar = obj.foo;
bar(); // a
上面的var bar = obj.foo引用的是foo函数的本身,不带任何修饰,所以调用bar时候,应该采用默认绑定的方式,即this指向全局(非严格模式)或者undefined(严格模式)。还有一种函数传参的方式也是一样的:
var name = "a";
function foo() {
console.log(this.name);
}
var obj = {
name: 'b',
foo: foo
}
function doAction(func) {
func()
}
doAction(obj.foo)//a
doAction传参时候,我们知道会有一个赋值操作,即 func = obj.foo,所以和上面一样,如果把函数传入内置函数,比如setTimeOut,还是一样的,没啥区别。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
setTimeout(obj.foo, 100); // "oops, global"
函数执行的时候,看它前面有没有点,如果有,点前面是谁,this就是谁,如果没有,就是window,间接引用的时候,很可能会被绑定到全局中
③ 显示绑定
改变this指向,在文章开头的时候就提到过了,apply和call都可以实现
我们可以理解为硬要绑定,强制绑定,如下例子:
var name = "a";
var obj = { name: "b" };
function foo() {
console.log(this.name);
}
var bar = function() {
foo.call(obj);
};
bar(); //b
setTimeout(bar, 100); //b
bar.call(window); //b
bar函数内部为foo强制绑定了this得指向,即obj。不管如何调用bar,到最后都要经过这一步的操作,所以this指向无法得到修改,一直都是指向obj。
❤❤❤❤❤❤应用场景❤❤❤❤❤❤
通过指定this的指向,包裹函数,传入参数进行操作,或者可以使用辅助函数
function foo(sth) {
return this.age + sth;
}
var obj = { age:2 };
var bar = function() {
return foo.apply( obj, arguments ); // 第二个参数是所有参数的数组
};
var b = bar( 3 );
console.log( b ); // 5
API调用的,一些第三方库,可以支持上下文的参数,确保回调函数使用我们指定的this
function foo(el) {
console.log( el, this.name );
}
var obj = { name: "guoguo" };
[1, 2, 3].forEach( foo, obj );
// 1 guoguo 2 guoguo 3 guoguo
数组中每个元素都会被当作foo的一个参数,逐个调用,并且在forEach的第二个参数中指定了this的指向
④new绑定
当我们使用new来调用对象函数时,会复制一份里面所有用this点的属性到新的对象中,因此新的对象就可以访问属性了
function foo(a) {
var b = a + 1;
this.a = a;
}
var bar = new foo(2);
console.log(bar.a, bar.b); // 2, undefined
注意点:
当this遇到了return,如果返回的是一个对象,那么this就是指向那个对象,否则还是原来的。返回undefined也不会改变指向。但是有一个特殊情况:null虽然是对象,但是还是指向原来的。
function foo(a){
this.a = a
return {} // undefined
// return function(){} undefined
// return null 1
// return undefined 1
}
var func = new foo(1)
console.log(func.a)
把基本数据类型转换为对应的引用类型的操作称为装箱,把引用类型转换为基本的数据类型称为拆箱。
优先级
- ① new 绑定的,this 绑定新创建的对象。
- ② call、apply显式绑定或者硬绑定调用的,this绑定指定的对象。
- ③ 隐式绑定的,this 绑定上下文对象。
- ④优先级最低,默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
这是是一些例外:————————
①当把null, undefined作为this的绑定对象传入call,apply,bind,实际应用的是默认的绑定规则
②当遇上间接引用的时候,调用这个函数会应用默认绑定规则
var a = 2;
function foo() {
console.log(this.a);
}
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3 隐式绑定
(p.foo = o.foo)(); // 2 使用全局的
③ 当this遇上了箭头函数
分析:foo函数内部的箭头函数会保留调用foo时候的this指向,此处则为obj1,而且指向无法被修改,所以就算硬绑定了obj2也没用。
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
箭头函数的this是根据外层函数决定的,如果外层还是还是一个箭头函数,就再往外层,直到全局