JS this指向以及call、apply、bind

this

值为当前执行上下文(global、functio或eval)的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。

描述

全局上下文

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。

// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b)  // "MDN"
console.log(b)         // "MDN"

函数上下文

在函数内部,this的值取决于函数被调用的方式。

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window

//在Node中:
f1() === globalThis;

然而,在严格模式下,如果进入执行环境时没有设置 this 的值,this 会保持为 undefined,如下:

function f2(){
  "use strict"; // 这里是严格模式
  return this;
}

f2() === undefined; // true

注:

  • 箭头函数比较特殊没有自己的 this,也不可以改变 this 的绑定,其 this 指向函数所在的作用域 。

类上下文

this 中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。

在类的构造函数中,this 是一个常规对象。类中所有非静态的方法都会被添加到 this 的原型中:

class Example {
  constructor() {
    const proto = Object.getPrototypeOf(this);
    console.log(Object.getOwnPropertyNames(proto));
  }
  first(){}
  second(){}
  static third(){}
}

new Example(); // ['constructor', 'first', 'second']

派生类

不像基类的构造函数,派生类的构造函数没有初始的 this 绑定。在构造函数中调用 super() 会生成一个 this 绑定,并相当于执行如下代码,Base为基类

this = new Base();

派生类不能在调用 super() 之前返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。

class Base {}
class Good extends Base {}
class AlsoGood extends Base {
  constructor() {
    return {a: 5};
  }
}
class Bad extends Base {
  constructor() {}
}

new Good();
new AlsoGood();
new Bad(); // ReferenceError

例题

40道this面试题

1.1

var a = 1
function foo () {
  var a = 2
  console.log(this)
  console.log(this.a)
}

foo() 
// Window 
// 1

1.2

var a = 1
function foo () {
  var a = 2
  function inner () { 
    console.log(this.a)
  }
  inner()
}

foo() 
// 1

2、隐式绑定的丢失问题

隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。

有两种情况容易发生隐式丢失问题:

  • 使用另一个变量来给函数取别名
  • 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定
2.1 使用另一个变量来给函数取别名
function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;

obj.foo();
foo2();
// 1 
// 2

var obj = { a: 1, foo };就相当于是var obj = { foo: foo },函数foo()虽然是定义在window下,但是我在obj对象中引用了它,并将它重新赋值到obj.foo上。且调用它的是obj对象,因此打印出来的this.a应该是obj中的a

这是因为虽然foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window

2.2 将函数作为参数传递
function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
// Window 
// 2
function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, doFoo }

obj2.doFoo(obj.foo)
// { a:3, doFoo: f }
// 2
var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }, 0)
  }
}
var a = 3

obj2.foo1()
obj2.foo2()
// 2
// Window
// 3

对于setTimeout中的函数,这里存在隐式绑定的隐式丢失,也就是当我们将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中的函数内的this是指向window的。

3、显示绑定call、apply、bind

如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数。

3.1
function foo () {
  console.log(this.a)
}
var obj = { a: 1 }
var a = 2

foo()
foo.call(obj)
foo.apply(obj)
foo.bind(obj)
// 2
// 1
// 1

foo.call()
foo.call(null)
foo.call(undefined)
// 2
// 2
// 2
var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }.call(obj1), 0)
  }
}
var a = 3
obj2.foo1()
obj2.foo2()
// 2
// { a: 1 }
// 1
3.2
function foo () {
  console.log(this.a)
}
var obj = { a: 1 }
var a = 2

foo()
foo.call(obj)
foo().call(obj)
// 2
// 1
// 2
// Uncaught TypeError: Cannot read property 'call' of undefined
  • foo()会正常打印出window下的a,也就是2
  • foo.call(obj)由于显式绑定了this,所以会打印出obj下的a,也就是1
  • foo().call(obj)开始会执行foo()函数,打印出2,但是会对foo()函数的返回值执行.call(obj)操作,可是我们可以看到foo()函数的返回值是undefined,因此就会报错了。

所以我们可以看到foo.call()foo().call()的区别了,一个是针对于函数,一个是针对于函数的返回值。

加上返回值看一下:

function foo () {
  console.log(this.a)
  return function () {
    console.log(this.a)
  }
}
var obj = { a: 1 }
var a = 2

foo()
foo.call(obj)
foo().call(obj)
// 2
// 1
// 2
// 1

4、箭头函数

var name = 'window'
var obj1 = {
	name: 'obj1',
	foo: function () {
		console.log(this.name)
	}
}

var obj2 = {
	name: 'obj2',
	foo: () => {
		console.log(this.name)
	}
}

obj1.foo()
obj2.foo()
// 'obj1'
// 'window'
  • 不使用箭头函数的obj1.foo()是由obj1调用的,所以this.nameobj1
  • 使用箭头函数的obj2.foo()的外层作用域是window,所以this.namewindow
var name = 'window'
var obj1 = {
  name: 'obj1',
  foo: function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2',
  foo: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}
var obj3 = {
  name: 'obj3',
  foo: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj4 = {
  name: 'obj4',
  foo: () => {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  }
}

obj1.foo()() // obj1 window
obj2.foo()() // obj2 obj2
obj3.foo()() // window window
obj4.foo()() // window window
  • obj1.foo()()两层都是普通函数,外层函数的调用者 obj1,外层函数的调用者为 window,分别打印出obj1window
  • obj2.foo()()外层为普通函数,内层为箭头函数,外层函数的调用者为 obj2,内层箭头函数的 this 由外层作用域决定,即为 obj2,分别打印出 obj2 、obj2
  • obj3.foo()()外层为箭头函数,内层为普通函数,箭头函数的this由外层作用域决定,因此为window,内层普通函数由调用者决定,调用它的是window,因此也为window
  • obj4.foo()()两层都是箭头函数,第一个箭头函数的this由外层作用域决定,因此为window,第二个箭头函数的this也由外层作用域决定,它的外层作用域是第一个箭头函数,而第一个箭头函数的thiswindow,因此内层的this也是window

call、apply、bind

call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组

语法

function.call(thisArg, arg1, arg2, ...)

参数

  • thisArg

    可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则不指定或指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

  • arg1, arg2, ...

    指定的参数列表。

返回值

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

示例

  • 使用 call 方法调用父构造函数

    function Product(name, price) {
      this.name = name;
      this.price = price;
    }
    
    function Food(name, price) {
      Product.call(this, name, price);
      this.category = 'food';
    }
    
    function Toy(name, price) {
      Product.call(this, name, price);
      this.category = 'toy';
    }
    
    var cheese = new Food('feta', 5);
    var fun = new Toy('robot', 40);
    
  • 使用 call 方法调用匿名函数

    var animals = [
      { species: 'Lion', name: 'King' },
      { species: 'Whale', name: 'Fail' }
    ];
    
    for (var i = 0; i < animals.length; i++) {
      (function(i) {
        this.print = function() {
          console.log('#' + i + ' ' + this.species
                      + ': ' + this.name);
        }
        this.print();
      }).call(animals[i], i);
    }
    
  • 使用 call 方法调用函数并且不指定第一个参数

    如果没有传递第一个参数,非严格模式下,this的值将会被绑定为全局对象‘;严格模式下,this 的值将会是 undefined。

    var sData = 'Wisen';
    
    function display() {
      console.log('sData value is %s ', this.sData);
    }
    
    display.call();  // sData value is Wisen
    

apply

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组)的形式提供的参数。

类数组:

  • 拥有length属性,其它属性(索引)为非负数
  • 不具有数组所具有的方法,可以通过Function.call或者Function.apply方法改变this指向,可以间接在类数组中使用数组的方法。
  • javascript中常见的类数组有 arguments对象和 DOM方法的返回结果。比如 document.getElementsByTagName()

参数

  • thisArg

    必选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

  • argsArray

    可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。

返回值

调用有指定**this**值和参数的函数的结果。

bind

**bind() **方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

function.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg

    调用绑定函数时作为 this 参数传递给目标函数的值。

  • arg1, arg2, ...

    当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值:

返回一个原函数的拷贝,并拥有指定的this值和初始参数。

示例

  • 偏函数

    只需要将预设的初始参数作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置。

    function list() {
      return Array.prototype.slice.call(arguments);
    }
    
    function addArguments(arg1, arg2) {
        return arg1 + arg2
    }
    
    var list1 = list(1, 2, 3); // [1, 2, 3]
    
    var result1 = addArguments(1, 2); // 3
    
    // 创建一个函数,它拥有预设参数列表。
    var leadingThirtysevenList = list.bind(null, 37);
    
    // 创建一个函数,它拥有预设的第一个参数
    var addThirtySeven = addArguments.bind(null, 37);
    
    var list2 = leadingThirtysevenList();
    // [37]
    
    var list3 = leadingThirtysevenList(1, 2, 3);
    // [37, 1, 2, 3]
    
    var result2 = addThirtySeven(5);
    // 37 + 5 = 42
    
    var result3 = addThirtySeven(5, 10);
    // 37 + 5 = 42 ,第二个参数被忽略
    

区别

  1. call 和 apply 会调用函数,并且改变函数内部 this 指向。
  2. cal 和 apply 传递的参数不一样, call 传递参数以 aru1, aru2…形式, apply 必须以数组形式 [ arg ]。
  3. bind 不会调用函数,可以改变函数内部 this 指向。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值