this指向问题和箭头函数总结长文

479 阅读8分钟

话不多说,直接正文。/狗头/

关于this指向问题:

1、函数是被谁调用,那么 this 就是谁,没有被对象调用,this 就是 window (注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined)

普通函数:
所有在全局作用域中声明的变量(var除let const)、函数都会变成window对象的属性和方法。
在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域。

// 例1
var a = 1 
function foo() {
  console.log(this.a)
}
foo()        // 1  没有被对象调用,this就是window

var obj = {
  a: 2,
  foo: foo
}
obj.foo()    // 2  谁调用指向谁,obj调用,指向obj

// 例2
var name = "windowsName";
function a() {
    var name = "Cherry";
    console.log(this.name);          // windowsName
    console.log("inner:" + this);    // inner: Window
}
a();
console.log("outer:" + this)         // outer: Window

// 例3
var name = "windowsName";
var a = {
    name: "Cherry",
    fn : function () {
        console.log(this.name);      // Cherry
    }
}
a.fn();
// 在这个例子中,函数 fn 是对象 a 调用的,所以打印的值就是 a 中的 name 的值

注意:
1.匿名函数的 this 永远指向 window。
2.setTimeout 方法是挂在window对象下的(除箭头函数)。“超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined”。
例如:

// 例4
var name = "windowsName";
var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)     
    },
    func2: function () {
        setTimeout(function () {
            this.func1()
        },100);
    }
};
a.func2()     // this.func1 is not a function
// 在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是 window,但是在 window 中并没有 func1 函数。
// 例5  箭头函数
var name = "windowsName";
var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)     
    },
    func2: function () {
        setTimeout(() => {
            this.func1()
        },100);
    }
};
a.func2()     // Cherry

2、以下情况是优先级最高的,this 只会绑定在 c 上,不会被任何方式修改 this 指向:

function foo() {
  console.log(this.a)
}
var a = 1 
var c = new foo()  // undefined   `this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
    c.a = 3
    console.log(c.a)  // 3   

在实际开发中,this 的指向可以通过四种调用模式来判断:

1、函数调用,当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象。

2、方法调用,如果一个函数作为一个对象的方法来调用时,this指向这个对象。

3、构造函数调用,this指向这个用new新创建的对象。

image.png

4、箭头函数,箭头函数的this绑定看的是this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。

怎么改变 this 的指向:

1.使用 ES6 的箭头函数:上面的例4和例5
2.在函数内部使用 _this = this (如果不使用 ES6,那么这种方式应该是最简单的不会出错的方式了, 我们是先将调用这个函数的对象保存在变量 _this 中,然后在函数中都使用这个 _this,这样 _this 就不会改变了。)
例如:

var name = "windowsName";
var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)     
    },
    func2: function () {
        var _this = this;
        setTimeout(function() {
            _this.func1()
        },100);
    }
};
a.func2()       // Cherry
这个例子中,在 func2 中,首先设置 var _this = this;,这里的 this 是调用 func2 的对象 a,
为了防止在 func2 中的 setTimeoutwindow 调用而导致的在 setTimeout 中的 thiswindow。
我们将 this(指向变量 a) 赋值给一个变量 _this,这样,在 func2 中我们使用 _this 就是指向对象 a 了。

3.使用 call、apply、bind :

var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)
    },
    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.call(a),100);
    }
};
a.func2()            // Cherry

var a = {
    name : "Cherry",
    func1: function () {
        console.log(this.name)
    },
    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.bind(a)(),100);
    }
};
a.func2()            // Cherry

4.new 实例化一个对象

箭头函数:

众所周知,ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。
箭头函数的 this 始终指向函数定义时的 this,而非执行时。
箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。

因为箭头函数没有 this,所以一切妄图改变箭头函数 this 指向都是无效的。
箭头函数的 this 只取决于定义时的环境。比如如下代码中的 fn 箭头函数是在 windows 环境下定义的,无论如何调用,this 都指向 window。

var a = 1
const fn = () => {
  console.log(this.a)
}  
fn()      // 1  this都指向window  申明的a属于全局变量等于 window.a=1
const obj = {
  fn,
  a: 2
}
obj.fn()  // 1  fn箭头函数是在 window 环境下定义的,无论如何调用,this都指向window

普通函数和箭头函数的区别:

1.箭头函数是匿名函数,不能作为构造函数,不能使用new来调用
2.箭头函数没有原型属性
3.箭头函数没有自己的this、super、arguments和new.target绑定
4.箭头函数没有this绑定,会捕获其所在的上下文的this值,作为自己的this值
5.箭头函数没有this,所以不能使用call、apply、bind来改变this的指向

为什么箭头函数不能作为构造函数?

构造函数是通过new关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this。
创建对象过程,new 首先会创建一个空对象,并将这个空对象的__proto__指向构造函数的prototype,从而继承原型上的方法,但是箭头函数没有prototype。因此不能使用箭头作为构造函数,也就不能通过new操作符来调用箭头函数。

new关键字

new一个对象发生了什么?

三步:

1.首先创建以这个函数为原型的空对象;将函数的prototype赋值给对象的__proto__属性;

2.它使this指向新创建的对象;

3.将对象作为函数的this传进去,如果有return就返回return里面的内容,如果没有就创建这个对象。

手写new:

function _new(fn, ...rest) {
  const obj = Object.create(fn.prototype)
  const res = fn.call(obj, ...rest)
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj
}

call,apply,bind区别:

语法:
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)

call/apply与bind的区别:
执行:
call/apply改变了函数的this上下文后马上执行该函数;
bind则是返回改变了上下文后的函数(新的函数),不执行该函数,需要手动调用。
返回值:
call/apply 返回fun的执行结果;
bind返回fun的拷贝,并指定了fun的this指向,保存了fun的参数,把传递进来的值用闭包存储起来,预处理机制。

call、apply的区别:
他们俩之间的差别在于参数的区别,call和aplly的第一个参数都是要改变上下文的对象,而call从第二个参数开始以参数列表的形式展现;
apply则是把除了改变上下文对象的参数放在一个数组里面作为它的第二个参数。

应用:

1.求数组中的最大和最小值

var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr);
Math.max.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
Math.min.apply(Math, arr);
Math.min.call(Math, 34,5,3,6,54,6,-67,5,7,6,-8,687);
// 也可用es6中的扩展运算符
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
console.log(Math.max(...arr))

2.判断数据类型

Object.prototype.toString.call(obj) === '[object Array]';

3.bind

for (var i = 1; i <= 5; i++) {
    // 缓存参数
    setTimeout(function (i) {
        console.log('bind', i) // 依次输出:1 2 3 4 5
    }.bind(null, i), i * 1000);
}

4.改变this指向

手写实现call()、apply()、bind():

call()

/* 
思路:
根据call的规则设置上下文对象,也就是this的指向。
通过设置context的属性,将函数的this指向隐式绑定到context上。
通过隐式绑定执行函数并传递参数。
删除临时属性,返回函数执行结果。
*/
Function.prototype.myCall = function(context, ...args) {
 // 正确判断函数上下文对象
  if (context === null || context === undefined) {
     // 指定为null和undefined的this值会自动指向全局对象(浏览器中为window)
     context = window 
  } else {
     // 值为原始值(数字,字符串,布尔值)的this会指向该原始值的实例对象
     context = Object(context)
  }
  // 创造唯一的key值,用于临时储存函数,作为我们构造的context内部方法名
  let fn = Symbol()
  // 函数的this指向隐式绑定到context上,改变构造函数的调用者间接改变this指向
  context[fn] = this
  // 通过隐式绑定执行函数并传递参数
  let result = context[fn](...args)
  // 删除上下文对象的属性
  delete context[fn] 
  // 返回函数执行结果
  return result  
} 

// 测试
let obj = { name: 'wu' }
function foo(...args) {
  console.log(this.name, args)
}
let s = foo.myCall(obj, '1', '2')

apply()

// apply原理一致  只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
  if (context === null || context === undefined) {
    context = window 
  } else {
    context = Object(context) 
  }
  let fn = Symbol()
  context[fn] = this
  let result = context[fn](...args)
  delete context[fn]
  return result
}

// 测试
let obj = { name: 'wu' }
function foo(...args) {
  console.log(this.name, args)
}
let s = foo.myApply(obj, [1, 2, 3, 4, 5])

bind()

/* 
思路:
1.拷贝源函数
2.返回拷贝的函数
3.调用拷贝的函数
*/
Function.prototype.myBind = function (context, ...args) {
  if (context === null || context === undefined) {
    context = window 
  } else {
    context = Object(context) 
  }
  let fn = Symbol()
  context[fn] = this
  let _this = this
  let result = function (...innerArgs) {
    if (this instanceof _this === true) {
      // 此时this指向指向result的实例 这时候不需要改变this指向
      this[fn] = _this
      // 这里使用es6的方法让bind支持参数合并
      this[fn](...[...args, ...innerArgs])  
    } else {
      // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
      context[fn](...[...args, ...innerArgs])
    }
  }
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(this.prototype)
  return result
}

// 测试
let obj = { name: 'wu' }
function foo(...args) {
  console.log(this.name, args)
}
let s = foo.myBind(obj, '1', '2')()
// bind是创建一个新的函数,我们必须要手动去调用