ES6-基础

338 阅读17分钟

ES6历程

ECMA-262 ECMAscript 脚本语言规范:

  • 1995 livescript -> JavaScript
  • 1996 JavaScript 1.0/1.1
  • 1997 JScript
  • 1997.6 ECMAscript 1.0
  • 1998.6 ECMAscript 2.0
  • 1999.12 ECMAscript 3.0
  • 2000 ECMAscript 4.0 草案没有通过(太过先进):TC39(technical committe 39)
  • 2007 ECMAscript 4.0 准备发布又被推迟
  • 2008.7 ECMAscript 3.1(在3.0的基础上进行小幅度升级)-> 更名ECMAscript5(大会项目代号hamony)
  • 2009.12 正式发布ECMAscript5
    • 内容还包括JavaScript.next(ES6) 和 JavaScript.next.next(ES7),都放入草案中
  • 2011.6 ECMAscript5.1
  • 2013.3 JavaScript.next(草案冻结)
  • 2013.6 JavaScript.next(草案发布)
  • 2015.6 ECMAscript6正式发布:从这开始每年6月份出来一个新版本升级

ES6的发布经历了十多年的时间,2015.6月份以来的所有版本(包括ECMAscript2016、ECMAscript2017、ECMAscript2019……)都是ES6的范畴。


let和块级作用域

var声明提升会存在很多问题,比如变量污染,在ES5中是靠用立即执行函数封闭作用域解决一部分问题的。

在ES6中,遵循kiss原则(keep it simple and stupid),产生了let语法和块级作用域。

let特征: 1. let不能在同一作用域下重复声明

var是可以的,只是会被覆盖

例1:

function test(a) {
    let a = 10;
    console.log(a);
}
test();

报错,因为在预编译的时候,a已经被定义过了,let不能重复定义。

例2:

function test(a) {
    {
        let a = 10;
    }
    console.log(a); //undefined
}

{}内部就是一个块级作用域,在外面无法调用

例3:

{
    let a = 1;
    function a(){}
}

这样也不行

例4:函数声明提升只会在当前的作用域提升,不会越出去的

{
    let a = 1;
    {
        a();//10
        function a(){
            console.log(10);
        }
    }
    console.log(a);//1
}

例5:

let x = 1;
function foo(y = x) {
    let x = 2;
    console.log(y);//1
}
foo()

参数赋值是在let x = 2前面的,所以拿不到2

例6:

let x = 1;
function foo(x = 2) {
    let x = 2;
    console.log(x);//报错
}
foo(5)

参数那已经定义过x了,类似于在函数的顶级作用域中已经用let声明过一次x了

例7:

let x = 1;
function foo(x = x) {
    console.log(x);//报错
}
foo()

()内的区域和花括号内的区域是一个作用域

类似于:

let x = 1;
{
    let x = x;
    console.log(x);
}

子级的x没有被定义,不会拿父级的x = 1


特征:2. let不会提升,会产生一个暂时性死区(TDZ - temporal dead zone)

例1:

console.log(a);//报错,var会打印undefined不会报错
let a = 10;

例2:

var a = a;
console.log(a); //undefined

let x = x; 
console.log(x); //报错 x is not defined

var a 会先提升,给a赋值为undefined,所以a = a =undefined

let不会提升,只能等它自右向左执行,将右边的x赋值给左边的x,但是这个时候x都还没有定义(let)他。在还没有定义的时候使用,就会报错

undefied 和 not defined 的区别,一个是没有赋值,一个是未被声明

例3:

function test(x = y, y = 2) {
    console.log(x, y);//报错,y赋值给x的时候还没被定义
}

修改:

function test(x = 2, y = x) {
    console.log(x, y);//2,2
}

typeof的问题

console.log(typeof a);//undefined

ES5不会因为没有声明变量的值报错,但是在ES6中typeof不再安全

console.log(typeof a);//报错
let a;

特征:3. let只能在当前的作用域下生效

例1:

function test(a) {
    let a = 10;
}
test();
console.log(a);//报错

例2:

for(;1;) {
    let a = 10;
}
console.log(a);//不报错

因为会一直死循环,就执行不到console……那行,所以不报错

在循环里加一个break就会报错了

例3:

for(let i = 0; i < 10; i++) {
    
}
console.log(i);//报错,let不会提升,不能提升到外面

for循环里,括号也是块级作用域

例4:

var arr = [];
for(var i = 0; i < 10; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
for(var i = 0; i < 10; i++) {
    arr[i]();//打印0-9
}

上面的for循环执行完之后是有10个函数的数组,下面的for循环执行数组项,执行数组项的时候,i被覆盖,用的是下面的函数的i!

即使上面的循环执行完毕之后i是10,但是我们不用啊

用let转译之后:

var arr = [];
var _loop = function _loop(i) {
    arr[i] = function() {  //这里相当于有一个闭包,使用的i是传入的参数的i
        console.log(i);
    }
}
for(var i = 0; i < 10; i++) {
    _loop(i); //每次循环都传入不一样的i,这样就不会有i都是10的问题了。
}
for(var i = 0; i < 10; i++) {
    arr[i]();//打印0-9
}

然后让我们回到let本身:

var arr = [];
for(let i = 0; i < 10; i++) {
    arr[i] = function() {
        console.log(i);
    }
}
for(var i = 0; i < 10; i++) {
    arr[i]();//打印0-9
}

这其实就相当于有闭包,子级作用域是不存在i的,那么就会去父级拿let缓存的i

{
    let i = 0;
    {
        arr[i] = function() {
            console.log(i);
        }
    }
}

也就是说子级作用域相当于一个封闭的函数作用域,i值以类似于参数的形式传进去,每一次循环传入的i值都是不一样的,打印的时候也能拿到传入的i。var就输在没有一个子级作用域,打印的时候只能拿到循环过后的i。


那么for循环里的括号和花括号都是块级作用域,是不是同一个作用域呢?

for(var i = 0; i < 10; i++) {
    i = 'a';
    console.log(i);//a
}

第一次循环i变成'a',第二次循环'a'进行隐式类型转换 —— NaN !< 10,所以循环只会进行一次。

for(let i = 0; i < 10; i++) {
    i = 'a';
    console.log(i);//a
}
for(let i = 0; i < 10; i++) {
    var i = 'a';
    console.log(i);//报错
}

看起来好像是在同一作用域下的,但其实只是因为var提升了,let不能重复定义

for(let i = 0; i < 10; i++) {
    let i = 'a';
    console.log(i);//10个a
}

但是经过这个例子可以看出不是一个作用域的。括号里的作用域其实是一个父作用域,两个作用域的i是不一样的,所以可以分别声明,有点类似下面的情况:

if(1) {
    let a = 1;
    {
        //let a = 10;
        console.log(a);//1
    }
}

con……是可以拿到父级作用域的值的,并且因为不在同一个作用域中,可以用let重新声明a,把注释拿掉是可以打印10的

你把ES6转成ES5就可以看到

if(1) {
    let a = 1;
    {
        let _a = 10;
        console.log(_a);//10
    }
}

这两个a根本不是一个东西

但是把子级的let改成var就会报错了,因为var是可以提升的,提到全局上,a就变成undefined了;到了if里面let又不能重新定义,所以会报错。

如果把子级作用域的let拿掉,因为没有let,相当于改值,a就是10

总结:let本质上就是为了给js增加一个块级作用域

ES6可以在块级作用域中嵌套函数

ES5中函数只能在顶层作用域和函数内部声明:

function test() {
    function test1() {
        //OK
    }
}
{
    function test1() {
        //ES5不能在块级作用域里声明函数,ES6可以
    }
}

但是不推荐这种方式,还是要用函数表达式的方式替代函数声明

try{
    var test1 = function() {...}
}catch(e){
    var test2 = function() {...}
}

块级作用域没有返回值

if(1){
    return a;//没有对象来接收
}

可以用do的方式返回值,但是目前只是一个草案

虽然可以用函数立即执行的方式来模拟块级作用域,但是块级作用域和立即执行函数不是同一个东西。


const常量

常量是不可以被改变的

比如引入模块,不希望模块被更改,只是调用模块里的方法

const test = require('http');

特征:

1. 一旦定义必须赋值

定义常量之后必须要给他赋值,不赋值不就说明可变了吗,与常量的概念冲突,所以不赋值是会报错的。

2. 有块级作用域,不能提升,有暂时性死区

console.log(a);  //报错
const a = 12;

3. 不能重复声明


Object.freeze()

如果保存的引用值,那么const只能保证栈指向堆的指针不变,不能保证引用值的内容不变。

const obj = {};
obj.name = 'jackson';
console.log(obj); //{name: 'jackson'}

如果要彻底保证对象不可变,就需要对对象进行冻结Object.freeze(obj)

const obj = {};
Object.freeze(obj);
obj.name = 'jackson';
console.log(obj); //{}

属性塞不进去了

那么如果对象里有属性也是一个对象的话,要怎么冻结呢?

function myFreeze(obj){
    Object.freeze(obj);
    for(var key in obj) {
        if (typeof(obj[key] === 'object') && obj[key] !== null) {
            Object.freeze(obj[key]);
        }
    }
}

但是这种方法很少用

一般会在源头上解决这个问题,也就是说模块最后是会返回一个实例化构造函数的。这样我们在引用模块的时候,不管怎么改,都不会影响模块(父级构造器,不要有引用值的问题)


顶层对象和全局变量

顶层对象的属性和全局变量是一样的,这其实会导致很多问题

比如说你声明并赋值了一个变量,后面想重新赋值的时候,不小心写错了,但是js是不会报错的,而是会挂到window下面的属性里

ES6就改变了这个状况

function、var允许声明的变量为全局变量

let、const、class不允许通过声明变量的方式声明全局变量的,不属于顶层对象的属性,默认生成一个块级作用域

顶层对象在不同的环境中是不一样的,例如在浏览器中是window,在node中是global


解构赋值

变量的解构,本质就是变量的赋值

前言

falsy虚值:经过boolean转换之后,值被认定为假的值

当下列函数传的是虚值的时候,会出现bug

function foo(x, y) {
    x = x || 1;
    y = y || 2;
    console.log(x + y);
}
foo(0, 5);  //6

所以要改成:

function foo(x, y) {
    var a = typeof(arguments[0]) !== 'undefined' ? arguments[0] : 1;
    var a = typeof(arguments[1]) !== 'undefined' ? arguments[1] : 2;
    console.log(a + b);
}

在ES6中为了优化这种写法,可以这么写:

function foo(x = 1, y = 2) {
    console.log(x + y);
}

例:

var w = 1, z = 2;
function foo(x = w + 1, y = x + 1, z = z + 1){
    console.log(x, y, z);//报错
}

子级作用域可以拿到父级作用域是w值,所以x = 1 + 1;y表达式的x可以拿到前面的x,没有问题;但是z = z + 1,后面的z只会拿前面的z值,不会到外层去拿,我里面有值为什么要到外面拿,但是这样z是不会被成功定义的,所以报错。

参数传值,每次都是经过重新计算的。参数为表达式的情况下,加载方式是一种惰性求值的方式。

例:

let a = 99;
function foo(b = a + 1) {
    console.log(b);
}
foo();//100
a = 100;
foo();//101

解构:其实也是一种赋值的过程,通过模式匹配(结构化赋值)简化赋值过程

数组解构

let [a, b, c] = [1, 2, 3];
let [d, [e], [f]] = [1, [2], [3]]; //只要结构一样就可以对应赋值

解构失败:变量多了,没有的值就用undefined来填充

let [d, [e], [f]] = [1, [], []];
console.log(d,e,f);//1,undefined,undefined

不完全解构:值多了

let [d, [], [f]] = [1, [2], [3]];
console.log(d,f);//1,3

解构赋值可以给默认值,除了undefined之外,都会用原本的值(js引擎会默认undefined是空,也就是没有填值)

let [a = 6] = [1];
console.log(a) //1

let [b = 8] = [];
console.log(b) //8

let [c, d = 2] = [1];
console.log(c, d); //1,2

let [e, f = 2] = [1, undefined];
console.log(e, f); //1,2

let [g, h = 2] = [1, null];
console.log(g, h); //1,null

解构的默认值可以是一个函数,但是函数默认的返回值是undefined。所以会先执行函数,打印10,然后返回undefined

function test(){
    console.log(10);
}
let [x = test()] = []; // 10 undefined

也可用变量

let [x = 1, y = x] = [];
console.log(x, y); //1,1

let [a = 1, b = a] = [2];
console.log(a, b); //2,2
let [x = y, y = 1] = [];
console.log(x, y); //报错,y没被定义

对象解构

var name = 'jackson';
var age = 10;
var person = {
    name: name,
    age: age,
    sex: 'male',
    eat: functuin(){
        console.log(1);
    }
}

在ES6中如果对象的属性名和值一致,可以简写;对象方法也可简写

var name = 'jackson';
var age = 10;
var person = {
    name,
    age,
    sex: 'male',
    eat(){
        console.log(1);
    }
}

ES6的对象还可以属性拼接

let firstName = 'wang';
let secondName = 'jackson';
let name = 'wang jackson';
let person = {
    [firstName + secondName] : name
}
console.log(person); //{wangjackson: "wang jackson"}

对象的解构

let {a: a, b: b, c: c} = {a: 1, b: 2, c: 3};
let {a, b, c} = {a: 1, b: 2, c: 3}; //简写

不完全解构 + 默认值

let {a = 2, b, c} = {b: 2, c: 3, e: 4, f: 5};
console.log(a, b, c); //2,2,3

解构失败

let {a = 2, b, c, d, e, f} = {b: 2, c: 3};
console.log(a, b, c, d, e, f); //2,2,3,undefined,undefined

数组的解构有顺序问题,对象没有

可以通过解构拿到JSON数据里的值

let[{"course": course1},{"course": course2}] = JSONData;
console.log(course1, course2);
var person = {
    name: 'zhangsan',
    age: 50,
    son: {
        name: 'lisi',
        ageLi: 30,
        son: {
            name: 'wangwu',
            ageWang: 12
        }
    }
}

怎样拿到最里面的儿子?

let {son: {son: son1}} = person;
console.log(son1);

如果属性名和变量一样的话,还可以简写

let {son: {son}} = person;
console.log(son);

一般我们还有这种写法

const {son} = person;
console.log(son);

这样我们就可以拿到person里面的整个son对象

如果要拿到李四和王五的年纪

let {son: {son:{ageWang}, ageLi}} = person;
console.log(ageLi,ageWang); //30,12

存在多层嵌套的问题,不要这么写

let {
    son: {
        son:{
            ageWang
        }, 
        ageLi
    }
} = person;

模式匹配

模式匹配还可以给对象的属性赋值

let a1 = [1, 2, 3], obj2 = {};
[obj2.a, obj2.b, obj2.c] = a1;

并且数组也是特殊的对象,也能够进行解构赋值

let arr = [1, 2, 3];
let {0: first, [arr.length - 1]: last} = arr;
console.log(first, last); //1,3

括号的问题

let {a} = {a: 1};
console.log(a);

如果想先声明a,像下面这样是不行的,js会把{}当成一个块

let a;
{a} = {a: 1};

添加括号变成表达式就可以了

let a;
({a} = {a: 1});

模式匹配本质上就是先声明一个变量,然后通过模式匹配的方式给变量赋值。


注意:用let/var声明,加了括号就报错(let没有被定义)

let ({a: b}) = {};
let {(a): b} = {};
[(b)] = [3];
console.log(b); //3
([b]) = [3];
console.log(b); //报错,模式不一样
({a: (b) = {}});
console.log(b); //{}

js认为(b) = {}是一个整体(默认值),所以这里本身并没有匹配,只是认为括号里有一个对象,属性a,默认值(b) = {}


例:如果要匹配属性的话,不能直接访问,而是要通过点语法访问

let obj1 = {a: 1, b: 2, c: 3}, obj2 = {};
({a: obj2.x, b: obj2.y, c: obj2.z} = obj1);
console.log(x, y, z);//报错
console.log(obj2.x, obj2.y, obj2.z); //1,2,3

例:设置属性和值

let a = 'x', b = 'y', obj = {};
({a: obj[a + b]} = {a: 2}); //要加括号
console.log(obj); //{'xy':2}

例:交换值

let a = 10, b = 20;
[b, a] = [a, b];

模式匹配可以匹配同源属性

let {a: x, a: y} = {a: 1};
console.log(x, y); //1,1

匹配的是同一个,而不是重复定义

重复定义是这样的:

let {a: x, a: x} = {a: 1};

例:

var x = 200, y = 300, z = 100;
var obj1 = {x: {y: 42}, z: {y: z}};

({y: x = {y: y}} = obj1); //obj1里面没有y,所以找默认值x = {y: y}
({z: y = {y: z}} = obj1);//匹配obj1里的z,z: {y: z}
({x: z = {y: x}} = obj1);//匹配obj1里的x,x: {y: 42}

console.log(x.y, y.y, z.y); //300,100,42

在解构中使用对象或数组,有了默认值代码的可读性就会下降,慎用!


体现在函数传参中

参数有没传完的地方就是undefined

function test([x, y]) {
    console.log(x, y);
}
test([]); //undefined,undefined

什么都不传就会报错,无法进行隐式转换

function test({x, y}) {
    console.log(x, y);
}
test({y: 2, x: 1}); //1,2

有默认值的情况:

function foo({x = 10} = {}, {y} = {y: 10}){
    console.log(x, y);
}
foo(); //10,10 这里不会报错的原因是有默认值存在
foo({}, {}); //10, undefined
foo({x: 2}, {y: 3}); //2,3 

{x:x = 10} = {},属性是不可能会赋值的,所以它一定简写了

{y:y} = {y: 10}


给函数参数赋予默认值会影响函数的length属性

function test(a, b, c = 1){}
test(1);
console.log(test.length); //2

它是不会算有默认值的参数和之后的参数的。如果c = 1放在第一个,长度就为0。放中间也会截断形参的长度。

实参和形参是相互映射的关系,改一个另外一个会跟着改。

function test(a, b, c, d, e, f){
    b = 7;
    console.log(arguments[1]); //7
}
test(1, 2, 3, 4, 5, 6);

但是如果给了默认值,映射关系就不存在了!


例:兼容不传值的写法

function foo({x, y = 5}){
    console.log(x,y);
}
foo({}); //undefined 5
foo({x: 1}); //1, 5
foo(); //报错

为了兼容最后一种写法,会给{x, y = 5}赋予一个默认值,如果传参为空,就赋予一个空对象

function foo({x, y = 5} = {}){
    console.log(x,y);
}
foo(); //undefined 5

例:搜索

function fetch(url, {body = "", method = "GET", header = {}} = {}) {  //属性简写了
    console.log(method);
}
fetch('http://www.baidu.com')

用户一般只会输入网址,不会输入配置项,所以就需要给配置项一个默认值{}


例:

var x = 1;
function foo(x, y = function(){x = 2; console.log(x)}){
    var x = 3;
    y(); //2
    console.log(x); //3
}
foo();
console.log(x); //1

在foo执行之前,全局对象上有x和foo

foo里面有两层作用域 foo:{local:{block}}

当foo执行到var x = 3之前。可以看到参数的x和执行体的x在不同的作用域里

y执行之前,块内的x赋值

y执行的前一刻,伴随着自己的作用域,还有foo的闭包(因为是在foo内部声明的,可以拿到形参的x)

y执行

在y中,x = 2

在foo中local环境中x = 2,block环境中,x依旧 = 3。所以x = 2这个赋值操作,并没有影响整个函数的 x = 3

所以说x = 2是让参数 = 2,外层和里层的作用域和x都不一样。

函数形成的时候,就定义了参数x和y,y函数里的x拿的是参数x。

如果把里层的var去掉,访问的就是上一层的local作用域,也就是将参数的x = 2改成3

var x = 1;
function foo(x, y = function(){x = 2; console.log(x)}){
    x = 3;
    y(); //2
    console.log(x); //3
}
foo();

解构的隐式转换

const [a, b, c, d, e] = 'hello';
console.log(a, b, c, d, e); //h e l l o

字符串转换为类数组,和数组一一对应

let {toString: s} = 233;
console.log(s);//f toString() {[native code]}

数字有对应的包装类,包装类是一个对象,Number的原型上有toString方法。

通过解构的方式,233进行了隐式转换

bool,number,string都可以进行隐式转换

null,undefined不可以


箭头函数表达式

基本形式:() => {}

特点:

  1. this指向根据外层函数的作用域决定的
  2. 不能作为构造函数来使用(没有this,所以也不能用call、apply、bind)
  3. 没有arguments对象,用rest(拓展运算符)替代
  4. yield命令不能生效,在generator函数中
let f = (a, b) => {
    return a + b;
};

当参数只有一个的情况下,可以把()省掉,当表达式只有一个的情况下,可以把{}省掉

var f = function(a) {
    return a;
}

等效于:

var f = a => a;

应用场景:

var arr = [54,85,32,855];
var arr1 = arr.sort((a, b) => a - b);

rest运算符

在箭头函数中不存在arguments!!!箭头函数并不是使用function来定义的,而是使用胖头‘=>’操作符来定义的,本质上是不一样的。

var sum = (a, b) => {
    console.log(arguments);
    return a + b;
}
sum(1, 2); //报错,arguments没有定义

那么替代方式就是.../rest/spread运算符:展开或收集

收集:参数 -> 数组

var sum = (...args) => {  //args是变量名称,自定义
    console.log(args); //打印出来是一个真真正正是数组[1,2],不是类数组
    return args[0] + args[1];
}
sum(1, 2);

收集剩下的所有参数,只能放到最后

let fn = (a, b, ...c) => {
    console.log(c); //[3,4,5,6]
}
fn(1, 2, 3, 4, 5, 6)

展开:数组 -> 参数

function foo (x, y, z) {
    console.log(x, y, z); //1,2,3
}
foo(...[1, 2, 3]);

用ES5实现这个功能:

foo.apply(null, [1, 2, 3]);

合并数组,如果数组就要用concat方法

let a = [2, 3, 4];
let b = [1, ...a, 5];
console.log(b); //[1,2,3,4,5]
[1].concat(a, [5]);

arguments是类数组,要应用数组上的方法还要先转一下

function sortNum(){
    Array.prototype.slice.call(arguments).sort(function(a, b){
        return a - b;
    })
}
console.log(sortNum(12,46,654,45,11,2));

...就不用,因为它返回的都是真正的数组

const sortNum = (...args) => args.sort((a, b) => a-b);

没有arguments,可以拿父级的arguments

function foo(){
    setTimeout( () => {
        console.log(arguments);//1,2,5,36,5
    } );
}
foo(1,2,5,36,5)

rest和默认值一样,都不能通过length访问到长度,length就不再准确了。

console.log((function(b, c, d = 0, ..a){}).length); //2

拓展运算符在ES2017上又有了新的拓展,能够将对象拓展开

var obj = {a: 1};
var obj1 = {a: 2, b: 3};
var obj2 = {};

之前是用assign合并

Object.assign(obj2, obj, obj1);

现在可以直接用...拓展

var obj2 = {
    ...obj,
    ...obj1
};

箭头函数 + 解构赋值

const full = ({first, last} = {}) => first + '' + last;
console.log(full({first: 3, last: 5})); //35

使用场景

改写箭头函数:

function insert(value) {
    return {
        into: function(array) {
            return {
                after: function(afterValue) {
                     array.splice(array.indexOf(afterValue) + 1, 2, value);
                    return array;
                }
            }
        }
    }
}
console.log(insert(5).into([1,2,3,4,6,7,8]).after(4))
let insert = (value) => ({
        into: (array) => ({
            after: (afterValue) => {
                array.splice(array.indexOf(afterValue) + 1, 2, value);
                return array;
            }
        })
})

这样的语义化实际上不是很好的

使用场景:

  1. 简单的函数表达式,得出唯一的return计算值,函数的内部没有this引用:没有递归、事件绑定、解绑定,用重构 => 的方式

例如sort这样返回值比较单一的情况,用一行语句就可以完成

  1. 内层函数表达式,需要调用this,var self = this,bind(this),确保适当的this指向的时候

  2. var args = Array.prototype.slice.call(arguments)

不适合箭头表达式的情况:函数声明,执行语句比较多的,还需要引用函数名,事件绑定、解绑定的情况,避免使用 =>


this

ES5的this指向:

  1. 默认绑定规则:函数内部this -> window,严格模式下this是undefined
  2. 隐式绑定:谁调用this就指向谁
function foo() {
    console.log(this.a);
}
var obj = {
    a:2,
    foo: foo
}
obj.foo(); //obj
var bar = obj.foo(); //因为这个时候obj.foo()把指针转交给了bar,也就是bar调用的方法
bar();// 报错,bar是全局上的方法,全局里没有a,所以会报错
  1. 显式绑定:call、apply、bind
  2. new,this指向实例化对象(优先级最高)

箭头函数的this指向:根据外层函数的作用域决定的

function foo(){
    return (a) => {
        console.log(this.a);
    }
}
var obj1 = {a: 2};
var obj2 = {a: 3};
var bar = foo.call(obj1);
bar.call(obj2);  //2

根据外层函数,也就是foo的作用域来决定

const person = {
    eat(){
        console.log(this);
    },
    drink: () => {
        console.log(this);
    }
}
person.eat();  //person
person.drink();   //window,外层的作用域是全局
;(function(){
    function Button(){
        this.button = document.getElementById('button');
    }
    Button.prototype = {
        init(){
            thid.bindEvent();
        },
        bindEvent(){
            //this.button.addEventListener('click', this.cliskBtn.bind(this), false);
            this.button.addEventListener('click', (e) => this.cliskBtn(e), false);
        },
        cliskBtn(e){
            console.log(e);
            console.log(this); //Button构造函数
        }
    }
    new Button().init();
})();

箭头函数的this指向就是Button实例化的时候确立的this指向

嵌套的时候的this指向

function foo(){
    return () => {
        return () => {
            return () => {
                console.log('id', this.id);
            }
        }
    }
}
var f = foo.call({id: 1});  //1
var f1 = f.call({id: 2})()(); //call不成功,找父级,1
var f2 = f().call({id: 3})(); //1
var f3 = f()().call({id: 4}); //1

箭头函数上其实没有this的机制,在箭头函数中this的指向是固化的,内部的this就是外层的this,套那么多层都是外面的this指向。

箭头函数的内部并没有自己的this,只能通过父级作用域来获取this,闭包(foo)的this

一个函数的执行导致另一个函数的定义,就会形成闭包(执行的那个函数foo)

内部的函数bind、apply、call都适用,根本就没有this,怎么绑定