概念
this是JavaScript中的一个关键字,它是函数运行时,在函数体内自动生成的一个对象,只能在函数体内部使用。this就是指针,指向我们调用函数的对象。
普通函数:谁调用(this)就指向谁
全局上下文
在全局执行上下文中(在任何函数体外部)this 都指代全局对象,浏览器的全局对象是 window。
// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true
this.name = "yz";
console.log(window.name) // "yz"
console.log(name) // "yz"
函数上下文 (在函数内部,this的值取决于函数被调用的方式。)
单独函数调用
函数的执行上下文是全局对象,相当于window.xxx()
var name = "window";
function f1() {
var name = "f1"
console.log(this.name); // window
}
f1(); // f1()的执行上下文是全局对象,相当于 window.f1()
构造函数调用(使用new关键字构建一个新的对象,this会绑定到这个新对象)
new操作符调用构造函数创建实例经历下面四个步骤:
- 1)创建一个新对象
- 2)将构造函数的作用域赋给新对象(因此this指向了这个新的对象)
- 3)执行构造函数中的代码(为这个新对象添加属性)
- 4)返回构造函数
var name = "window";
function f1() {
console.log(this.name); // undefined
}
new f1(); // this指向 new 创建的对象,this.name未定义
function f2() {
this.name = "f2";
}
var fn = new f2();
console.log(fn.name); // "f2"
对象方法的调用
// 函数作为对象方法调用
var person={
age:20,
getAge(){
var age = 30;
return this.age;
},
};
person.getAge() === person.getAge.call(person) // 答案是20
解析:
getAge作为对象person的方法被 person 调用的,所以 this 指向 person的对象。
定时器setTimeout和setInterval(超时调用的代码时在全局作用域中执行的,严格模式下是undefined,非严格模式下是window对象)
var name = "window";
var Obj = {
name: "obj",
foo: function() {
setTimeout(function() {
console.log(this.name);
}, 1);
}
}
Obj.foo(); // "window"
超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined。
如果使用箭头函数的话,this 是会绑定到obj这个上下文,如下:
var name = "window";
var Obj = {
name: "obj",
foo: function() {
setTimeout(() => console.log(this.name), 0);
}
}
Obj.foo(); // "obj"
this 的四种绑定规则
默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高
默认绑定
- 独立函数调用时,this指向全局对象。
- 严格模式,全局对象无法使用默认绑定,this绑定为undefined
function foo() {
console.log( this.a );
}
var a = 2;
foo() ==== foo.call(window) // 2 默认绑定,因为foo的调用不属于任何人,前面没有任何限定条件。
答案:2
解析:
因为foo()是直接调用的(独立函数调用),没有应用其他的绑定规则,这里进行了默认绑定,将全局对象绑定this上,所以this.a 就解析成了全局变量中的a,即 2。
⚠️:严格模式下
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // Uncaught TypeError: Cannot read property 'a' of undefined
隐式绑定
- 当函数引用有上下文对象时(即函数作为引用属性被添加到对象中)
- 隐式绑定规则会把函数调用中的this,绑定到这个上下文对象
function foo() {
console.log( this.a );
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
obj.foo(); // 3 隐式绑定。Foo是作为obj方法而调用的,那么谁调用foo,this就指向谁。
答案:3
解析:
这里foo函数被当做引用属性,被添加到obj对象上。这里的调用过程是这样的:
获取obj.foo属性 --> 根据引用关系找到foo函数,执行调用.
所以这里对foo的调用存在上下文对象obj,this进行了隐式绑定,即this绑定到了obj上,所以this.a被解析成了obj.a,即3。
多层调用链
function foo() {
console.log(this.a);
}
var obj2 = {
a: 2,
fn: foo
};
var obj1 = {
a: 1,
o1: obj2
};
obj1.o1.fn(); // 2
答案:2
解析:
如果是链性的关系,比如 xx.yy.obj.foo();, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个
obj1对象的o1属性值是obj2对象的地址,而obj2对象的fn属性的值是函数foo的地址; 函数foo的调用环境是在obj2中的,因此this指向对象obj2;
显式绑定
通过这两个方法call(…)或apply(…)来实现,会将this绑定到这个对象上
function foo() {
console.log( this.a );
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
foo.call( obj1 ); // 3
foo.call( obj2 ); // 4
答案: 3,4
因为显式的申明了要绑定的对象,所以this就被绑定到了obj上,打印的结果自然就是obj1.a 和obj2.a。
function foo() {
console.log( this.a );
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
var bar = function(){
foo.call( obj1 );
}
setTimeout( bar, 100 );
bar.call( obj2 ); // 3
虽然bar被显示绑定到obj2上,对于bar,function(){…} 中的this确实被绑定到了obj2,而foo因为通过foo.call( obj1 )已经显示绑定了obj1,所以在foo函数内,this指向的是obj1,不会因为bar函数内指向obj2而改变自身。所以打印的是obj1.a(即3)
new 绑定
(1)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var a = 2;
var bar1 = new foo(3);
console.log(bar1.a); // 3
var bar2 = new foo(4);
console.log(bar2.a); // 4
答案:3,4
解析: 每次调用生成的是全新的对象,该对象又会自动绑定到this上
(2)如果原函数返回一个对象类型,那么将无法返回新对象,将丢失绑定this的新对象。
function foo(){
this.a = 10;
return new String("捣蛋鬼");
}
var obj = new foo();
console.log(obj.a); // undefined
console.log(obj); // "捣蛋鬼"
this绑定的优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
总结this四种绑定规则
- 1.默认绑定。严格模式下,this指向的是undefined,否则绑定的是全局对象
var bar = foo()
- 2.隐式绑定。函数是否在某个上下文对象中调用?是,this绑定的是那个上下文对象
var bar = obj1.foo()
- 3.显式绑定。函数是否通过call()、apply()绑定?是,this绑定的是指定的对象
var bar = foo.call(obj2)
- 4.new绑定。函数是否在new中调用?是,this绑定的是新创建的对象
var bar = new foo()
普通函数改变this指向
只有在普通函数中,通过 call,apply,bind 这几种方法,才可以改变 this 指向。
在 javascript 中,call和apply、bind都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
它们各自的定义:
apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。
call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。
apply( )
apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组
function add(a,b){
return a+b;
}
function sub(a,b){
return a-b;
}
var a1 = add.apply(sub,[4,2]); //sub调用add的方法
var a2 = sub.apply(add,[4,2]);
alert(a1); //6
alert(a2); //2
obj 是作为函数上下文的对象,函数 func 中 this 指向了 obj 这个对象。参数 A 和 B 是放在数组中传入 func 函数,分别对应 func 参数的列表元素。
call()
call 方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组.
function add(a,b){
return a+b;
}
function sub(a,b){
return a-b;
}
var a1 = add.call(sub,4,2); //sub调用add的方法
var a2 = sub.call(add,4,2);
alert(a1); //6
alert(a2); //2
bind()
接受的参数有两部分,第一个参数是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数
var obj = {
name: 'linxin'
}
function func(firstName, lastName) {
console.log(firstName + ' ' + this.name + ' ' + lastName);
}
func.bind(obj, 'C', 'D'); // C linxin D
多个调用
var age = 10;
var person={
age:20,
getAge:function(){
var age = 30;
return this.age;
},
};
person.getAge.call(person); // 20
这里在执行 getAge 方法的时候,传入了 person,那么 getAge 的 this 指向 person,所以输出 20。
综合例子:
var age = 10;
var person={
age:20,
child:{
age:40,
getAge:function(){
return this.age;
},
},
child2:{
age:40,
getAge:()=>{ // 箭头函数,没有自己的this,逐级向上查找,找到函数作用域的this,则为当前肩头函数的this
return this.age;
},
},
child3:function(){
this.getAge =()=>{
return this.age;
}
}
};
console.log(person.child.getAge());// 40
console.log(person.child2.getAge()); // 10
console.log((new person.child3()).getAge()); // undfined
解析:
1、person.child.getAge() 的 this 就是谁调用指向谁,这里是 child 调用,所以指向 child ,输出 40
2、箭头函数是没有自己的 this,就是当前箭头函数逐级向上查找,找到函数作用域的 this,则为当前箭头函数的 this。这里 person.child2.getAge 函数的父级调用方是 child2,但是 child2 是对象,也没有自己的 this 和 作用域,所以继续向上查找 person,然后发现 person 也是对象,再继续向上查找,找到 window 这个大 Boss 了,所以 this 就指向 window ,输出为 10
3、(new person.child3()).getAge() 的 this ,同理向上一级查找,发现 new person.child3() 是个函数实例,所以 this 指向 child3 的这个实例,然而 child3 实例没有 age 属性,所以输出 undefined。
箭头函数:调用者指向谁,(this)则指向谁。指向函数声明时所在作用域下的对象,而不是运行时所在的对象。
总结:
| 类型 | this指向 |
|---|---|
| 声明式 function fun(){} | window |
| 赋值式 var fun = function(){} | window |
| forEach()循环 | window |
| 定时器,延时器 setInterval(function(){} , 时间) | window |
| 对象中的函数 const obj = {fun:function(){}} | obj对象 |
| 事件处理函数 标签.addEventListener(事件类型,function(){}) | 标签对象 |
箭头函数的this指向,是父级程序的this指向
如果父级程序有this指向,指向的就是父级程序的this指向如果父级程序没有this指向(*对象,数组是没有this*),指向的是window
⚠️:箭头函数,无法改变this指向
var age = 10;
var person={
age:20,
getAge:()=>{
var age = 30;
return this.age;
},
};
person.getAge(); // 10
解析: 这个的 getAge 方法是 person 调用的,则 getAge 和 person 的指向一致,person 是 window 调用的(参照上述普通函数),所以 person 指向 window,因此 getAge 也指向 window,输出 10。
var person={
age:20,
getAge:()=>{
var age = 30;
return this.age;
},
};
person.getAge(); // undefined
答案:undefined
解析:getAge 方法是 person 调用的,则 getAge 和 person 的指向一致,person 是 window 调用的(参照上述普通函数),所以 person 指向 window,window中没有定义age的值,所以就是undefined.
箭头函数与普通函数的区别
声明方式不同
- 普通函数需要使用关键字function完成,并且使用function既可以声明成一个具名函数又可以生成一个匿名函数
- 箭头函数只需要使用=>就可以
- 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名
this指向不同
- 普通函数,内部的this指向函数运行时所在的对象
- 箭头函数没有自己的this对象,内部的this是定义时上层作用域中的this
箭头函数不能直接使用argument类数组对象,但是如果箭头函数的外层还有普通函数,那么箭头函数的参数就等于外层第一个普通函数的参数。
arguments是个类数组对象,包含着传入函数的所有参数。
解决方案:箭头函数可以使用扩展运算符来展开参数
var fun = (...args) => {
console.log(args);
}
fun(1,2,3); // [1,2,3]
箭头函数是匿名函数,不能使用new操作符
因为:箭头函数是匿名函数,是不能作为构造函数的,不能使用new
var B = ()=>{
value:1;
}
var b = new B(); //TypeError: B is not a constructor
箭头函数没有原型属性
var a = ()=>{
return 1;
}
function b(){
return 2;
}
console.log(a.prototype); // undefined
console.log(b.prototype); // {constructor: ƒ}
call()、apply()、bind()不会改变this的指向
箭头函数不能当作Generator函数,不能使用yield关键字
箭头函数使用场景
- 定时器
- 数组回调
箭头函数不适合的场景
- 不适合---对象方法
const obj = {
name: '哪吒,B站,算法猫叔',
getName: () => {
return this.name
}
}
console.log(obj.getName())
- 不适用--原型方法
const obj = {
name: '哪吒,B站,算法猫叔'
}
obj.__proto__.getName = () => {
return this.name
}
console.log( obj.getName() )
- 不适用--构造函数
const Foo = (name, age) => {
this.name = name
this.age = age
}
const f = new Foo('张三', 20)
// 报错 Foo is not a constructor
- 不适用--动态上下文中的回调函数
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
// console.log(this === window)
this.innerHTMl = 'clicked'
})
额外记录
取数组中的极大值、极小值
var num = [6,9,-3,-5];
console.log(Math.max.apply(Math,num)); // 9 等价 console.log(Math.max(6,9,-3,-5));
console.log(Math.min.apply(Math,num)); // -5 等价 console.log(Math.min(6,9,-3,-5));
合并数组
var a = [1,2,3];
var b = [4,5,6];
[].push.apply(a,b); // 借用数组的push方法 等价 a.push(4,5,6);
console.log(a); // [1, 2, 3, 4, 5, 6]
面试题
var name = 222
var a = {
name:111,
say:function(){
console.log(this.name)
}
}
var fun = a.say
fun() // fun.call(window) 222
a.say() //a.say.call(a) 111
var b = {
name:333,
say:function(fun){
fun() // fun.call(window) 222
}
}
b.say(a.say)
b.say = a.say
b.say() // b.say.call(b) 333
var Test ={
foo:"test",
func:function () {
var self=this;
console.log(this.foo); // test
console.log(self.foo); // test
(function () {
console.log(this.foo); // undefined
console.log(self.foo); // test
})();
}
};
Test.func();
var name = '南玖'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name) }
},
this.foo4 = function () {
return () => {
console.log(this.name) }
}
}
var person1 = new Person('nan')
var person2 = new Person('jiu')
person1.foo1() // 'nan'
person1.foo1.call(person2) // 'jiu'
person1.foo2() // 'nan'
person1.foo2.call(person2) // 'nan'
person1.foo3()() // '南玖'
person1.foo3.call(person2)() // '南玖'
person1.foo3().call(person2) // 'jiu'
person1.foo4()() // 'nan'
person1.foo4.call(person2)() // 'jiu'
person1.foo4().call(person2) // 'nan'
解析:
- 执行
person1.foo1(),foo1为普通函数,所以this应该指向person1,打印出nan - 执行
person1.foo1.call(person2),foo1为普通函数,并且用call改变了this指向,所以它里面的this应该指向person2,打印出jiu - 执行
person1.foo2(),foo2为箭头函数,它的this指向上层作用域,也就是person1,所以打印出nan - 执行
person1.foo2.call(person2),箭头函数的this指向无法使用call改变,所以它的this还是指向person1,打印出nan - 执行
person1.foo3()(),这里先执行person1.foo3(),它返回了一个普通函数,接着再执行这个函数,此时就相当于在全局作用域中执行了一个普通函数,所以它的this指向window,打印出南玖 - 执行
person1.foo3.call(person2)()这个与上面类似,也是返回了一个普通函数再执行,其实前面的执行都不用关心,它也是相当于在全局作用域中执行了一个普通函数,所以它的this指向window,打印出南玖 - 执行
person1.foo3().call(person2)这里就是把foo3返回的普通函数的this绑定到person2上,所以打印出jiu - 执行
person1.foo4()(),先执行person1.foo4()返回了一个箭头函数,再执行这个箭头函数,由于箭头函数的this始终指向它的上层作用域,所以打印出nan - 执行
person1.foo4.call(person2)(),与上面类似只不过使用call把上层作用域的this改成了person2,所以打印出jiu - 执行
person1.foo4().call(person2),这里是先执行了person1.foo4(),返回了箭头函数,再试图通过call改变改变该箭头函数的this指向,上面我们说到箭头函数的this始终指向它的上层作用域,所以打印出nan