一、前言
深入学习的过程是 快乐
的!!!
面试问到了箭头函数和普通函数的区别,去阮一峰ES6中看相关资料,发现主要还是指出 this
的指向问题,于是去深入理解this,相关文章又提出了执行上下文,调用栈等概念,顺着这个线索学下去发现盲区越来越多。。
本文主要介绍JavaScript中 this
关键字,原文地址:js 五种绑定彻底弄懂this,默认绑定、隐式绑定、显式绑定、new绑定、箭头函数绑定详解
二、绑定方式
在ECMAScript 6之前没有箭头函数,JavaScript中绑定方式主要包括四种,分别是 默认绑定
,隐式绑定
, 显式绑定
, new绑定
几种,ES6中提出了箭头函数,因此this绑定多出了一种形式,让我们一探究竟吧
1. 默认绑定
所谓的默认绑定,指的函数在全局作用域中运行时,this默认指向window对象的情况(非严格模式),严格模式下指向undefined
如下面例子(非严格模式):
var name = "rambler";
function test() {
console.log(this); // window对象
console.log(this.name) // rambler
}
test();
严格模式下的结果:
var name = "rambler";
function test() {
"use strict"
console.log(this); // undefined
console.log(this.name) // Uncaught TypeError: Cannot read property 'name' of undefined
}
test();
严格模式
只是在ECMAScript 5中提出的用在脚本或者函数顶部的一种指令或者声明,有点类似html文档头部的<!DOCTYPE html>
声明在严格模式下代码有一些限制,比如未声明不能赋值,不能通过
delete
删除变量和属性等,这里不一 一罗列
2. 隐式绑定
什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上,如果函数调用前存在多个对象,this指向距离调用自己最近的对象
function fun() {
console.log(this.name);
}
let obj = {
name: "test",
fun: fun
}
obj.fun(); // test
可能这样写更容易理解?
let obj = {
name: "test",
fun: function() {
console.log(this.name);
}
}
obj.fun(); // test
其实上面说的多个对象调用this指向最近的对象,和这个是一个意思,例子:
let obj = {
name: "外层",
test: {
name: "内层",
fun: function() {
console.log(this.name);
}
}
}
obj.test.fun(); //内层
文章原文中指出了一个问题,将是将函数引用赋值给一个变量,通过这个变量调用这个函数this将丢失
看这个例子
let test = {
name: "内层",
fun: function() {
console.log(this.name);
}
}
let fun = test.fun;
fun(); // 没有输出
test.fun(); // 内层
将test.fun赋值给fun,在通过fun()调用this就失效
其实这里可以这样理解,test.fun是一个函数的引用,fun这个变量就是保存的这个函数的引用,直接通过fun调用,就相当于全局执行这个函数,类似这样
function fun() {
console.log(this.name);
}
fun();
修改代码,打印this,会发现通过变量引用对象里面的函数,this是指向window对象,印证了我的说法
但是存在一个问题就是不太明白为什么打印的空,而不是undefined,因为window对象中明明没有name属性
3. 显式绑定
所谓的显式绑定就是指主动绑定函数调用时的this,主要时通过apply,call和bind进行绑定,三者的区别不是本文的重点,只将this的问题
上个简单例子
function fun() {
console.log(this.name);
}
let obj1 = {
name: "apply方式绑定"
}
let obj2 = {
name: "call方式绑定"
}
let obj3 = {
name: "bind方式绑定"
}
fun.apply(obj1); // apply方式绑定
fun.call(obj2); // call方式绑定
let newFun = fun.bind(obj3);
newFun(); // bind方式绑定
通过这三中方式手动绑定this,这里需要注意的是call和apply绑定this后会自动执行,而bind绑定后返回的是一个函数,可以通过一个变量接受,并执行,或者直接在后面加上括号运行,
fun.bind(obj3)()
bind是硬绑定,也就是说通过bind绑定this后,后续无论是通过bind还是apply都无法修改this指向
4. new 绑定
准确来说,js中的构造函数只是使用new 调用的普通函数,它并不是一个类,最终返回的对象也不是一个实例,只是为了便于理解习惯这么说罢了。new的过程如下:
比如我声明一个Person构造函数
function Person() {
this.id = "123";
}
Person.prototype.name = "test";
然后通过new关键字创建一个Person的 实例
var person = new Person
这期间都发生了什么呢?实际过程如下
// 伪代码
var person = {}; // 创建一个空对象
person.__proto__ = Person.prototype; // 将person对象的原型链指向Person的原型链
Person.call(person); // 通过call改变this指向person对象
这就很容易理解new中this绑定在谁身上了,也就是刚创建的对象
通过打印创建的person对象发现person对象有id属性,证明this确实指向person对象
5. 箭头函数绑定
箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁。而且一旦绑定,便无法修改,类似bind硬绑定
记住一句话, 箭头函数就是个吃软饭的,外层this指向谁,他的this就指向谁
简单例子:
var name = "111"
let fun = () => {
console.log(this.name);
}
let obj = {
name: "test"
}
fun.call(obj);
这个箭头函数fun是在全局作用域中创建的,因此this指向window对象,所以打印的结果不是test,而是window对象的name属性,也就是111
第二个例子
function fun() {
var test = () => {
console.log(this.name)
}
return test;
}
var obj = {
name: "test",
}
fun.call(obj)();
注意:箭头函数所在函数this指向谁,箭头函数的this就指向谁
上面例子最后一行,fun.call(obj)这句话通过显式绑定将fun函数的this指向obj对象,fun函数中又返回了一个箭头函数,所以箭头函数也指向obj对象,因此打印结果是 test
三、this绑定优先级问题
new无法和显式绑定同时存在,因为通过new操作符调用函数返回的是一个对象,而不是一个函数,因此无法在通过call和apply等更改this指向
例如下面的代码会报错
function Person() {};
let obj = {};
new Person().call(obj);//Uncaught TypeError: (intermediate value).call is not a function
因此有下面的结论:
显式绑定 > 隐式绑定 > 默认绑定
new绑定 > 隐式绑定 > 默认绑定