this指向详解

164 阅读4分钟

一、默认绑定的情况(函数直接调用)

1、非严格模式下执行:

function fn() {
  console.log(this) // window对象
}

fn()
//面试时可能迷惑一下
var a = 1
function fn() {
  var a = 2
  console.log(this.a) // 1
  //因为this指向的是window
}
fn()
//如果将上面的变量a赋值换成let,结果会是什么呢?
let a = 1
function fn() {
  var a = 2
  console.log(this.a) // undefined
  //因为使用var声明会将变量绑定到全局,所以可以在window上找到a,而let声明,并不会将变量绑定到全局,所以window.a 是 undefined
}
fn()
//再多套几层函数还会吗?
var b = 1
function outer () {
  var b = 2
  function inner () { 
    console.log(this.b) // 1
  }
  inner()
  //因为inner函数是直接调用,所以函数内的this会指向window,和嵌套在几层函数内无关
}
outer()
//函数在对象内
const obj = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}

obj.fn() // 1
//对象的方法里调用时,this指向调用该方法的对象.
const f = obj.fn
f() // undefined
//属于函数直接调用的情况

2、严格模式下执行:

function fn() {
  'use strict'
  console.log(this) // undefined
}

fn()

二、隐式绑定(属性访问调用)

function fn () {
  console.log(this.a)
}
const obj = {
  a: 1
}
obj.fn = fn
obj.fn() // 1
//隐式绑定的 `this` 指的是调用堆栈的上一级(`.`前面第一个对象)

下边这个例子更直接表现
function fn () {
  console.log(this.a)
}
const obj1 = {
  a: 1,
  fn
}
const obj2 = {
  a: 2,
  obj1
}
obj2.obj1.fn() //1

在一些个别情况下,隐式绑定的 this指的是调用堆栈的上一级(.前面第一个对象)的说法并不完全正确,需要对应的区分

1、赋值的情况

const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}
const fn1 = obj1.fn // 函数引用给了fn1,等同于写了 function fn1() { console.log(this.a) }
fn1() // 所以这里其实已经变成了默认绑定规则了,该函数 `fn1` 执行的环境就是全局环境

2、setTimeout的情况

const obj1 = {
  a: 1,
  fn: function() {
    console.log(this.a)
  }
}
setTimeout(obj1.fn, 1000) //this指向的是全局

3、 函数作为参数传递

function run(fn) {
  fn()
}
run(obj1.fn) // 这里传进去的是一个引用,相当于直接执行函数,所以指向的还是全局this

4、匿名函数

var name = 'The Window';
var obj = {
    name: 'My obj',
    getName: function() {
        return function() { // 这是一个匿名函数
            console.log(this.name)  //The Window
        };
    }
}
obj.getName()()

三、显式绑定(callbindapply

通过一些方法去强行的绑定 this 上下文,例如:

function fn () {
  console.log(this.a)
}

const obj = {
  a: 100
}

fn.call(obj) // 100
这种根本还是取决于第一个参数,第一个为 `null` 的时候还是绑到全局的

call、 bind、 apply三个单独的知识点

1、bind
MDN中定义如下

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

  var a = {
    b: function() {
      var action = function() {
        console.log(this.c);
      }
      action();
    },
    c: 'bind'
  }
  a.b(); // undefined 这里的this指向的是全局作用域
  console.log(a.c); // bind
  var a = {
    b: function() {
      var _this = this; // 通过赋值的方式将this赋值给that
      var action = function() {
        console.log(_this.c);
      }
      action();
    },
    c: 'bind'
  }
  a.b(); // bind
  console.log(a.c); // bind
// 使用bind方法
   var a = {
    b: function() {
      var action = function() {
        console.log(this.c);
      }
      action.bind(this)();
    },
    c: 'bind'
  }
  a.b(); // bind
  console.log(a.c); // bind
 

分析几个例子

// 分析:这里的bind方法会把它的第一个实参绑定给f函数体内的this,所以里的this即指向{x:1}对象;
// 从第二个参数起,会依次传递给原始函数,这里的第二个参数2即是f函数的y参数;
// 最后调用m(3)的时候,这里的3便是最后一个参数z了,所以执行结果为1+2+3=6
// 分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)
  function f(y,z){
    return this.x+y+z;
  }
  var m = f.bind({x:1},2);
  console.log(m(3)); // 6
// 分析:直接调用a的话,this指向的是global或window对象,所以会报错;
// 通过bind或者call方式绑定this至document对象即可正常调用
  var a = document.write;
  a('hello'); // error
  a.bind(document)('hello'); // hello
  a.call(document,'hello'); // hello
// 实现预定义参数
// 分析:Array.prototype.slice.call(arguments)是用来将参数由类数组转换为真正的数组;
  function list() {
    return Array.prototype.slice.call(arguments);
  }
  var list1 = list(1, 2, 3); // [1,2,3]
// 第一个参数undefined表示this的指向,第二个参数10即表示list中真正的第一个参数,依次类推
  var a = list.bind(undefined, 10);
  var list2 = a(); // [10]
  var list3 = a(1, 2, 3); // [10,1,2,3]

参考链接:bind