重学ES6基础语法(一)

1,095 阅读9分钟

本列博客为ES6基础语法的使用及总结,如有错误,欢迎指正。 重学ES6基础语法(一)主要包括 var、let、const变量声明箭头函数 相关。

var/let/const 变量声明

var的使用

1.基本用法

  • 1.1 定义变量格式:var 变量名称 = 变量值,如果没有给变量赋值,默认为undefined
  • 1.2 用 var 声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。
  • 1.3 通过var定义变量,可以重复定义同名变量,不会报错;后定义的会覆盖先定义的。
  • 1.4 通过var定义在{}(块级作用域)的变量,不区分全局变量和局部变量,后续仍可以使用。
  • 1.5 通过var定义变量,可以先使用后定义(预解析)
//定义一个变量
var a;

//重复定义同名变量
var a = 18;
var a = 16;
console.log(a); //输出16

2.变量提升(hoisting)

  • 2.1 通过var声明的变量可以在声明之前使用,输出undefined(也就是我们常说的可以先使用后定义)
  • 2.2 这个行为叫做“hoisting”,也就是我们说的预解析。把所有的变量声明移动到函数或者全局代码的开头位置。
console.log(a); //输出undefined
var a = 18

//预解析之后
var a;
console.log(a); //输出undefined
a = 18

3.注意点

3.1 通过var声明的变量属于函数作用域,如果当前没有函数,则属于全局作用域。在外界是可以访问到的。

 var price = 10;
 var count = 10;
 if(count > 5){
     var discount = price * 0.8; //discount是一个全局变量,在控制台可以访问到
     console.log(`the discount is ${discount}`);
 }

如果换成let定义:通过let定义的discount是一个局部变量,在控制台不可以访问

var price = 10;
var count = 10;
if(count > 5){
    let discount = price * 0.8; 
    console.log(`the discount is ${discount}`);
}

3.2 window对象上有一个叫做name的属性,如果使用var定义一个叫做name的属性,那么就会覆盖掉window的name属性。

var name = "mss";

解决:可以通过立即执行函数(IIFE (Immediately Invoked Function Expression) ),使变量私有化,避免属性重写的问题。此时输出的name就是window上面的name属性。

但是,利用立即执行函数仅仅为了声明一个变量,避免变量重写有点大材小用了...请避免这样的用法

(function () {
   var name = 'mss';
   console.log(name);
})()

let的使用

1.基本用法

  • 1.1 通过let不可以重复定义同名变量。
  • 1.2 通过let定义变量,不可以先使用再定义,浏览器不会对let定义的变量进行预解析。会输出ReferenceError
  • 1.3 通过let定义在{}的变量区分全局变量和局部变量
  • 1.4 通过let定义在{}的局部变量,只能在{}中使用,后续不可以使用,只在块级作用域内有效。
//不可以先使用再定义
console.log(a); //ReferenceError
let a = 18; 

//不可重复定义
let a = 18;
let a = 88;
console.log(a); //Identifier 'a' has already been declared

//块级作用域内区分全局变量和局部变量
{
   let a = 18; //局部变量
   var person = 'jelly'; //全局变量
   console.log(a); //18
}
console.log(a); //ReferenceError
console.log(person); //jelly

2.注意点

2.1 let不会在全局声明时(在最顶部的范围)创建window 对象的属性。 在程序和方法的最顶端,let不像 var 一样,let不会在全局对象里新建一个属性。 位于函数或代码顶部的var声明会给全局对象新增属性, 而let不会。例如:

var x = 'global';
let y = 'global';
console.log(this.x); // "global"
console.log(window.x); // "global"
console.log(this.y); // undefined
console.log(window.y); // undefined

2.2 在同一作用域内通过let定义变量,绝不允许同名变量出现;包括用var声明的变量。

{
  let name = "jelly";
  var name = "vicky"; //Identifier 'name' has already been declared
}


let name = "jelly";
{
  var name = "vicky"; //Identifier 'name' has already been declared
}

{
  let name = "jelly";
  {
    let name = "vicky"; //不报错
  }
}

3.暂存死区(TDZ(Temporal dead zone))

与通过 var 声明的有初始化值 undefined 的变量不同,通过 let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

4.一道很切题的题

var a = 5; //var定义的a绑定到了window对象上面
let obj = {
    a: 10,
    foo: function () {
        console.log(this.a); 
    }
};
let bar = obj.foo;
obj.foo(); //10 obj调用了foo函数,this就是obj对象,输出10
bar(); //5 bar函数是输出当前对象上面的a,为5

let a = 5; //let定义的a不会绑定到window对象上面
let obj = {
    a: 10,
    foo: function () {
        console.log(this.a);
    }
};
let bar = obj.foo;
obj.foo(); //10 obj调用了foo,this就是obj对象,输出10
bar(); //undefined 当前的this是window对象,但是window上面没有a属性,输出undefined 

一道奇怪写法的笔试题

var a = 5;
function test() {
    a = 0;
    console.log(a); //0
    console.log(this.a); //undefined
    var a;
    console.log(a); //0
}
new test();

上例中,在函数内声明变量a=0,在后面又使用var a,会进行变量提升,所以在函数内部就存在一个局部变量a了,所以两次console.log(a)都输出0;console.log(this.a)输出undefined可以理解为没有东西调用test函数,导致this的指向不明确,也就找不到this.a属性。

const的使用

1.用法

  • 1.1 const 用于声明一个或多个常量,常量的值不可改变。例如亘古不变的PI,可以用const定义。
  • 1.2 const定义常量与使用let 定义的变量有着惊人的相似之处:
    • 二者都是块级作用域
    • 都不能和它所在作用域内的其他变量或函数拥有相同的名称
var x = 10;
{
    const x = 2;
    console.log(x); //2
}
console.log(x); //10
  • 1.3 两者还有以下两点区别:
    • const声明的常量必须初始化,而let声明的变量可以不用初始化
    • const 定义常量的值不能通过再赋值修改,也不能再次声明。而 let 定义的变量值可以修改。
// 错误写法
const PI;
PI = 3.14159265359;

// 正确写法
const PI = 3.14159265359;

2.const注意点之面试现场还原

使用 const 定义的对象或者数组,其实是可变的。对象的引用(地址)不可更改,但是对象的属性可以改变;只是我们不能对常量对象重新赋值

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

// 创建常量对象
const car = {type:"Fiat", model:"500", color:"white"};
 
// 修改属性:
car.color = "red"; // 合法
 
// 添加属性
car.owner = "Johnson"; // 合法

const car = {type:"Fiat", model:"500", color:"white"};
car = {type:"Volvo", model:"EX60", color:"red"};    // 错误

varletconst的使用场景

  • 默认变量尽量使用const来定义(use const by default)
  • 需要重新绑定时使用let定义(only use let if rebinding is needed)
  • 在ES6中尽量避免使用var(var shouldn't be used in ES6)

箭头函数(Arrow Function)

1.箭头函数的写法

//没有参数
() => {函数声明}

//一个参数
单一参数 => {函数声明}

//多个参数
(参数1, 参数2, …, 参数N) => { 函数声明 }

2.箭头函数特点

2.1 隐式返回

什么是显示返回?
显示返回格式:return “返回的内容”

2.1.1 箭头函数的隐式返回:

const arr = [2,4,6,8,10];
const double = arr.map(function (number) {
    return number *2;
});
console.log(double);
//等价于
const double2 = arr.map((number) => number*2);
console.log(double2);

2.1.2 注意:箭头函数都是匿名函数,如果想要通过命名函数的方式使用箭头函数,一般把箭头函数赋值给一个变量即可

const greet = name => console.log(`hello ${name}`);
greet("mss");

2.2 不绑定this

const person = {
    name: 'mss',
    hobbies: ['money','sleeping','eating'],
    output: function () {
        //console.log(this); //this是person这个对象
        this.hobbies.map(function (hobby) {
            //console.log(this);//this是window
            console.log(`${this.name} loves ${hobby}`);
        })
    }
};
person.output();
/*   输出结果:
     loves money
     loves sleeping
     loves eating
*/

上述例子中可以看出,并没有输出name属性。在output的函数中,this指向person这个对象,而map方法的回调函数是一个独立的函数,一个独立函数运行的时候,没有作为对象的方法调用或者没有使用call等修改this的值时,则此时this指向window; 过去的做法是:在外面保存一下this

const person = {
    name: 'mss',
    hobbies: ['money','sleeping','eating'],
    output: function () {
        let that = this;
        this.hobbies.map(function (hobby) {
            console.log(`${that.name} loves ${hobby}`);
        })
    }
};
person.output();
/*  输出结果
    mss loves money
    mss loves sleeping
    mss loves eating
*/

2.2.1 解决:在ES6中可以用箭头函数来解决这种问题

2.2.2 原因:

  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。也就是他的父作用域的this
  • 与过去的this值是在运行时动态指定的不同,箭头函数的this是词法作用域,在定义时就被指定了,在后续也不会随着调用方法的改变进行改变。
const person = {
    name: 'mss',
    hobbies: ['money','sleeping','eating'],
    output: function () {
        this.hobbies.map(hobby => {
            console.log(`${this.name} loves ${hobby}`);
        })
    }
};
person.output();
/*  输出结果
    mss loves money
    mss loves sleeping
    mss loves eating
*/

2.2.3 应用:函数节流/防抖中使用箭头函数简化代码

//防抖
function debounce(fn,delay) {
   let timerId = null;
   return function (...args) {
       timerId && clearTimeout(timerId);
       timerId = setTimeout(()=>{
           fn.apply(this,args);
       },delay || 3000);
   }
}

//节流
function throttle(fn,delay) {
   let timerId = null;
   let flag = true;
      return function (...args) {
         if(!flag) return;
         flag = false;
         timerId && clearTimeout(timerId);
         timerId = setTimeout(()=>{
             flag = true;
             fn.apply(this,args);
         },delay || 3000);
     }
}

2.3 不绑定Arguments

箭头函数不绑定Arguments 对象,即箭头函数没有Arguments对象。

3.不推荐使用箭头函数的场景

3.1 作为构造函数或者是一个对象的方法或者给原型绑定方法的时候,是不允许使用箭头函数的

箭头函数没有prototype属性。

let Foo = (name,age) => {
    this.name = name;
    this.age = age;
};
let foo = new Foo('ghk',22); // TypeError: Foo is not a constructor
Foo.prototype.say = () => {
    console.log(`hello ${this.name}`); // TypeError: Foo is not a constructor
}

3.1.1 原因: 通过new生成一个实例的时候,在内部会完成四个步骤

(1)创建一个新的对象

(2)将构造函数中的作用域赋值给新对象(构造函数的this就指向了该新生成的对象)

(3)执行构造函数中的代码(为新对象添加属性)

(4)返回新对象

箭头函数用作构造器时,并未完成把this值绑定到新生成的对象上面去这个步骤,所以会报错,因此只能使用原始函数作为构造函数。 正确写法:

let Foo = function(name,age){
    this.name = name;
    this.age = age;
};
let foo = new Foo('ghk',22); 
Foo.prototype.say = function (){
    console.log(`hello ${this.name}`);
};
foo.say();

3.2 需要使用this的时候(交互的时候),不推荐使用箭头函数

给元素绑定事件时,不可用箭头函数。因为此时箭头函数的this是window,而触发事件this应该指向到触发事件的该元素上。 错误写法:

let oBtn = document.querySelector(".btn");
oBtn.addEventListener("click",() => {
  console.log(this); //this是window,所以会报错
  this.classList.add("in");
  setTimeout(() => {
      console.log(this); //this是oBtn
      this.classList.remove("in");
  },2000)
})

正确写法:

let oBtn = document.querySelector(".btn");
oBtn.addEventListener("click",function () {
    console.log(this); //this是oBtn
    this.classList.add("in");
    setTimeout(() => {
        console.log(this); //this是oBtn
        this.classList.remove("in");
    },2000)
})

3.3 需要使用Arguments对象的时候,不推荐使用箭头函数

箭头函数中没有Arguments对象 错误写法:

let sum = () =>{
    return Array.from(arguments)
        .reduce((prevSum,curValue) => prevSum + curValue);
};
sum(1,2,3); //arguments is not defined

正确写法:

let sum = function(){
    return Array.from(arguments)
        .reduce((prevSum,curValue) => prevSum + curValue);
};
console.log(sum(1, 2, 3)); //6

或者

let sum = (...args) => {
    return args.reduce((prevSum,curValue) => prevSum + curValue);
};
console.log(sum(1, 2, 3));

3.4 不可以使用 yield命令,因此箭头函数不能用作Generate函数。


补充知识

Array.from()作用是把类数组对象转换为真数组

  • 语法:Array.from(arrayLike[, mapFn[, thisArg]])
  • 参数:①arrayLike:想要转换成数组的伪数组对象或可迭代对象;
    ②mapFn (可选参数):如果指定了该参数,新数组中的每个元素会执行该回调函数;
    ③thisArg (可选参数):可选参数,执行回调函数 mapFn 时 this 对象。
console.log(Array.from('foo'));
// expected output: Array ["f", "o", "o"]

console.log(Array.from([1, 2, 3], x => x + x));
// expected output: Array [2, 4, 6]

reduce()为数组中的每一个元素依次执行callback函数,不包括数组中被删除或从未被赋值的元素

  • 语法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

  • 参数: 第一个参数:callback函数

    callback函数里面的参数有:①accumulator 累计器;②currentValue 当前值;③currentIndex 当前索引(可选);④array 数组(可选)

    第二个参数:initialValue(可选)

    作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

let arr = [1,2,3,4];
let sum = (accumulator,currentValue) => accumulator + currentValue;
console.log(arr.reduce(sum)); //10
console.log(arr.reduce(sum, 5)); //15

上面的代码中,sum函数的参数accumulator是累积变量,参数currentValue是当前的数组成员。每次执行时,currentValue会加到accumulator,最后输出accumulator。 reduce方法中的第二个参数不传的时候,初始值默认使用数组中的第一个元素。即1+2+3+4输出10;当初始值为5时,即5+1+2+3+4输出15.


本文章参考到的链接:

developer.mozilla.org/en-US/docs/…

www.ruanyifeng.com/blog/develo…

www.runoob.com/