JS 普通函数与箭头函数中this场景

549 阅读13分钟

概念

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