学习ES6新语法

509 阅读28分钟

ES6新语法

前言:


ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。


ECMAScript的历史

ES6 从开始制定到最后发布,整整用了 15 年。

ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是** 2000 年**。

为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。

2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。

2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。

2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。

2011 年 6 月,ECMAscript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。

2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。

2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

等待了多年,总算等到了ES6,下面介绍一部分的新语法,本文介绍的语法如下:

1. 块级作用域

由于在ES5之前,没有块级作用域,有些时候很不方便,所以ES6新增了块级作用域。

块级声明是指该声明的变量无法被代码块外部访问。块作用域,又被称为词法作用域(lexical scopes),可以在如下的条件下创建:

  1. 在函数内部定义
  2. 在代码块(即 {} )内部定义

块级作用域是很多类C语言的工作机制,ECMAScript 6 引入块级声明的目的是增强 JavaScript 的灵活性,同时又能与其它编程语言保持一致.

1.1 Let声明

let定义块级作用域变量

1.没有变量的提升,必须先声明后使用.

2.let声明的变量,不能与前面的let,var,conset声明的变量重名


   //全局执行上下文
    {
    //局部执行上下文
    //console.log(a)//此处浏览器报错未声明
    let a = 10;//let定义只能在当前上下文使用
    var b = "abc";//全局作用域变量
    console.log(a);//10
    //let a = 10//浏览器提示错误 Uncaught SyntaxError: Identifier 'a' has already been declared
    console.log(b);//bac
    }
    console.log(b);//abc
    // console.log(a);//浏览器提示错误


1.2 Const声明

const 定义只读变量

1.const声明变量的同时必须赋值,const声明的变量必须初始化,一旦初始化完毕就不允许修改

2.const声明变量也是一个块级作用域变量

3.const声明的变量没有“变量的提升”,必须先声明后使用

4.const声明的变量不能与前面的let,var,const声明的变量重名

{
    const con = "abc"
    console.log(con)//也只能在当前执行上下文中使用
}
//console.log(con)//con is not defined  浏览器提示错误,con未声明

1.3 循环中的块级绑定

使用var声明的循环变量在循环结束后仍然可以访问到。 使用let声明的循环变量,在循环结束之后会立即销毁。

    for(let i = 0; i < 3; i++){ // 循环结束之后会立即销毁 i
        console.log(i);
    }
    console.log(i);  //此处无法访问到 i 。

1.4 循环中的函数

看下面的代码,是输出10个10,而不是0,1,2,…

    var funcs = [];
    for (var i = 0; i < 10; i++) {
        funcs.push(function () {
            console.log(i);
        });
    }
    funcs.forEach(function (func) {
        func();     // 输出 "10" 共10次
    });

解决办法需要使用函数的自执行特性。

var funcs = [];
for (var i = 0; i < 10; i++) {
    funcs.push((function(value) {
        return function() {
            console.log(value);
        }
    }(i)));
}
funcs.forEach(function(func) {
    func();     // 输出 0,1,2 ... 9
});

如果使用let声明变量,则完全可以避免前面的问题。 这是ES6规范中专门定义的特性。在for … in和for … of循环中也适用

    var funcs = [];
    for (let i = 0; i < 10; i++) {
        funcs.push(function () {
            console.log(i);
        });
    }
    funcs.forEach(function (func) {
        func();     // 输出 0,1,2 ... 9
    })

说明:let 声明使得每次迭代都会创建一个变量 i,所以循环内部创建的函数会获得各自的变量 i 的拷贝。每份拷贝都会在每次迭代的开始被创建并被赋值。

2.神奇的解构赋值

2.1 什么是解构

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。解构只能用于数组或对象,所以应该注意,其他原始类型的值都可以转为相应的对象 ,除了undefined和null本质上。

解构写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

2.2 对象的解构

2.2.1 对象解构的基本形式

对象结构的语法就是在赋值语句的左侧使用类似对象字面量的结构。

let node = {
    name: "阿猫",
    age:20
};
//这里就相当于声明了两个变量: type = node.type;  name:node.name
let { name,age } = node;

console.log(name);      // "阿猫"
console.log(age);      // 20

在上面的结构中必须要初始化。否则会出现语法错误。


var { name,age };// 语法错误!

let { name,age };// 语法错误!

const { name,age };// 语法错误!

2.2.2 解构赋值表达式

如果声明的变量想改变他们的值,也可以使用解构表达式。

let node = {
  name: "阿猫",
  age:20
},
name = "阿狗",
age = 5;

//注意:此处必须要在圆括号内才能使用解构表达式
({ name,age } = node);

console.log(name);      // "阿猫""
console.log(age);      // 20

2.2.3 对象解构时的默认值

如果赋值号右边的对象中没有与左边变量同名的属性,则左边的变量会是 undefined

let node = {
  name: "阿猫",
  age:20
};
//因为node中没有叫value的属性,所以valued的值将会是undefined
let { name, age, value } = node;

console.log(name);      // "阿猫"
console.log(age);      // 20
console.log(value);     // undefined

不过我们也可以手动指定他的默认值。(这个和函数的参数默认值很像)

let node = {
  name: "阿猫",
  age:20
};
//手动添加value的默认值为3
let { name, age, value = 3} = node;

console.log(name);      // "阿猫"
console.log(age);      // 20
console.log(value);     // 3

2.2.4 赋值给不同的变量名

在前面的操作中,都是把对象的属性值,赋值给同名变量。

其实也可以赋值给不同名的变量。

let node = {
  name: "阿猫",
  age:20
};
// localName才是要定义的新的变量。  name是node的属性
let {name: localName, age: localAge} = node;

console.log(localName);     // "阿猫"
console.log(localAge);     // 20

注意:冒号后面才是要定义的新的变量,这个可以我们的对象字面量不太一样!

这个地方也可以使用默认值。

let node = {
        type: "Identifier"
    };

let { type: localType, name: localName = "bar" } = node;

console.log(localType);     // "Identifier"
console.log(localName);     // "bar"

2.3 数组的解构

2.3.1 数组解构基本语法

数据解构的语法和对象解构看起来类似,只是将对象字面量替换成了数组字面量,而且解构操作的是数组内部的位置(索引)而不是对象中的命名属性,例如:

let colors = [ "red", "green", "blue" ];
let [ firstColor, secondColor ] = colors;

console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

如果只想取数组中的某一项,则可以不用命名。

let colors = [ "red", "green", "blue" ];
//只取数组中的第三项。
let [ , , thirdColor ] = colors;

console.log(thirdColor);        // "blue"

2.3.2 解构表达式

你可以想要赋值的情况下使用数组的解构赋值表达式,但是和对象解构不同,没必要将它们包含在圆括号中,例如:

let colors = [ "red", "green", "blue" ],
    firstColor = "black",
    secondColor = "purple";

[ firstColor, secondColor ] = colors;  //可以不用加括号。当然添加也不犯法

console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

数组解构表达式有一个很常用的地方,就是交换两个变量的值。在以前一般定义一个第三方变量进行交换,例如下面的代码:

let a = 3,
    b = 4,
    temp;
temp = a;
a = b;
b = temp;
console.log(a);
console.log(b)

那么在ES6中完全可以抛弃第三方变量这种方式,使用我们的数组解构表达式

let a = 3,
    b = 4;
//左侧和前面的案例是一样的,右侧是一个新创建的数组字面量。
[a, b] = [b, a];
console.log(a);
console.log(b)

2.4 解构JSON

var jike = {"name":"tom","age":"23","sex":"男"};
var {name,age,sex}=jike;
console.log(name,age,sex)//tom 23 男

function cal(a,b){
    var ret1 = a+b;
    var ret2 = a-b;
    var ret3 = a*b;
    var ret4 = a/b;
    return [ret1,ret2,ret3,ret4]
}
var [r1,r2,r3,r4] = cal(10,5);
console.log(r1,r2,r3,r4)//15 5 50 2

3. 字符串功能的增强

3.1 查找子字符串

  1. includes(要查找的文本) 判断字符串内是否有此文本
  2. startsWith(要查找的文本) 判断文本是否在字符串开头
  3. endsWith(要查找的文本) 判断文本是否在字符串结尾

使用如下:

var msg = "Hello world!";

console.log(msg.startsWith("Hello"));       // true
console.log(msg.startsWith("o"));           // false
console.log(msg.startsWith("o", 4));        // true

console.log(msg.endsWith("!"));             // true
console.log(msg.endsWith("world!"));        // true
console.log(msg.endsWith("r", 9));          // true

console.log(msg.includes("o"));             // true
console.log(msg.includes("x"));             // false
console.log(msg.includes("o", 8));          // false

  1. trim():除去字符串左右空格的。
  2. trimLeft():除去字符串的左边空格
  3. rimRight():除去字符串的右边空
var msg = " Hello "

console.log(msg)//" Hello "
console.log(msg.trim())//"Hello"
console.log(msg.trimLeft())//"Hello "
console.log(msg.trimRight())//" Hello"

一般配合正则表达式,用的比较的。

  1. repeat(复制的次数):复制字符串
var msg = "abc"
console.log(msg.repeat(3));//abcabcabc

  1. padStart(补齐的位数,填充的的字符):用提供的字符,向前补齐位数
var msg = "abc"
console.log(msg.padStart(10,"*"));//*******abc

  1. padEnd(补齐的位数,填充的的字符):用提供的字符,向前补齐位数
var msg = "abc"
console.log(msg.padEnd(10,"*"));//abc*******

3.2 模板字符串

模板字面量是 ECMAScript 6 针对 JavaScript 直到 ECMAScript 5 依然缺失的如下功能的回应:

多行字符串 针对多行字符串的形式概念(formal concept)。 基本的字符串格式化 将字符串中的变量置换为值的能力。 转义 HTML 能将字符串进行转义并使其安全地插入到 HTML 的能力。 模板字面量以一种全新的表现形式解决了这些问题而不需要向 JavaScript 已有的字符串添加额外的功能。

3.2.1 基本语法

使用一对反引号 “(tab正上方的按键)来表示模板字面量。

let message = `Hello world!`;   //使用模板字面量创建了一个字符串

console.log(message);               // "Hello world!"
console.log(typeof message);        // "string"
console.log(message.length);        // 12

注意:如果模板字符串中使用到了反引号,则应该转义。但是单双引号不需要转义

3.2.2 多行字符串

在ES5之前JavaScript是不支持多行字符串的。(但是在以前的版本中有一个大家都认为是bug的方式可以写出多行字符串,就是在尾部添加一个反斜杠 \)

    var s = "abc \
    aaaaaa";
    console.log(s); //但是输出的结果中不包括换行

但是在ES6中字符串的模板字面量轻松的解决了多行字符串的问题,而且没有任何新的语法

    var s = `abc
    aaaaa
    dsalfja
    dfadfja`;
    console.log(s);

但是要注意: 反引号中的所有空格和缩进都是有效字符。

4. 数组的扩展

4.1 数组推导

数组推导就是直接通过现有数组生成新数组的一种简化写法,通过for...of结构,允许多重循环。注:新数组会立即在内存中生成,这时如果原数组是一个很大的数组,将会非常耗费内存。

var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];
a2 // [2, 4, 6, 8]

var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];
[for (year of years) if (year > 2000 && year < 2010) year];
// [ 2006]

4.2 数组处理扩展方法

Array.from():用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象,其中包括ES6新增的Set和Map结构。Array.from()还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理。

Array.from({ 0: "a", 1: "b", 2: "c", length: 3 });
// [ "a", "b" , "c" ]

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.of()方法用于将一组值,转换为数组。弥补数组构造函数Array()的不足。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]

Array(3) // [undefined, undefined, undefined]

数组实例的find()用于找出第一个符合条件的数组元素;数组实例的findIndex()用于返回第一个符合条件的数组元素的位置。这两个方法都可以发现NaN,弥补了IndexOf()的不足。

数组实例的fill()使用给定值,填充一个数组。

数组实例的entries(),keys()和values()用于遍历数组,它们都返回一个遍历器,可以用for...of循环进行遍历。keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

5. 对象功能的拓展

5.1 创建对象

ES5中创建对象的几种方法

  1. 字面量法
  2. 工厂模式
  3. 构造函数
  4. 组合方式 构造函数+原型模式

在ES6中可以使用class创建一个对象

class 类名{
    constructor(参数){
        this.属性 = 参数;        
    }  
    running{//对象中简写方法,省略了function
        
    }
}

注意:

  1. lass 是关键字,后面紧跟类名,类名首字母大写,采取的是大驼峰命名法则。类名之后是{}。
  2. 在{}中,不能直接写语句,只能写方法,方法不需要使用关键字。
  3. 方法和方法之间没有逗号。不是键值对

5.2 新增的方法

5.2.1 属性相关的方法

(1) Obect.getOwnProertyDescriptor();

获取一个对象中某个属性的详细信息。


var a = 1;
console.log(Object.getOwnPropertyDescriptor(window,"a"))

浏览器打印如下:


{value: 1, writable: true, enumerable: true, configurable: false}
    configurable:false
    enumerable:true
    value:1
    writable:true
    __proto__:Object

(2) Object.defineProperty( )

精细化设置一个对象的属性的

  1. configurable:是否可删除
  2. writable:是否可修改
  3. enumerale:是否可以枚举
  4. value:值

var obj = {};
Object.defineProperty(obj,"name",{
    configurable:false,
    writable:true,
    enumerale:false,
    value:"阿毛"
})

浏览器打印如下:

> delete obj.name;
< false
> obj.name = "阿猫"
< "阿猫"
> obj.name
< "阿猫"


(3) Object.defineProperties()
var obj = {};
Object.defineProperties(obj,{
   "name":{
       configurable:false,
       writable:false,
       enumerable:false,
       value:"阿毛"
   },
   "age":{
       configurable:true,
       writable:true,
       enumerable:true,
       value:"20"
   }
});

浏览器打印如下:

> obj
< {age: "20", name: "阿毛"}
> delete obj.name;
< false
> delete obj.age
< true
> obj
< {name: "阿毛"}

(4) getOwnPropertyNames()

获取自的属性,以数组的格式返回的。

var obj = { 
   name:"阿毛",
   age:20
};

浏览器打印如下:


> Object.getOwnPropertyNames(obj);
< (2) ["name", "age"]
    0: "name"
    1: "age"
    length: 2
    __proto__: Array(0)

(5) Object.keys()
var obj = { 
       name:"阿毛",
       age:20
};

浏览器打印如下:


> Object.getOwnPropertyNames(obj);
< (2) ["name", "age"]
   0: "name"
   1: "age"
   length: 2
   __proto__: Array(0)

使用Object.getOwnPropertyNames()和Object.keys()都可以得到一个对象的属性名,属性名是放在一个数组中的。

对于对象的遍历目前有三种方式:

  • for in
  • Object.keys()
  • Object.getOwnPropertyNames()

for in : 会输出自身以及原型链上可枚举的属性。 Object.keys() : 用来获取对象自身可枚举的属性键 Object.getOwnPropertyNames() : 用来获取对象自身的全部属性名

(6) Object.values();

获取对象的值,放到数组中。

var obj = { 
       name:"阿毛",
       age:20
};

浏览器打印如下:

> Object.values(obj);
< (2) ["阿毛", 20]

5.2.2 继承相关的方法

(1) Object.create()

使用Object.create比较适合对字面量对象的继承。

(2) getPrototypeOf

getPrtotypeOf 获取创建这个对象的那个构造器的prototype属性。


var obj = {
    name : "阿毛",
    age : 20
}
console.log(Object.getPrototypeOf(obj) == Object.prototype);
console.log(Object.getPrototypeOf([]) == Array.prototype);

true
true

5.2.3 防篡改方法

可以对对象进行保护,分成三个级别:

(1) 设置不可拓展: preventExtensions()

Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。如下:

var obj = {
    name : "阿猫",
    age : 20
}
Object.preventExtensions(obj);
obj.name = "阿狗";
delete obj.age;
obj.sex = "man";
console.log(obj);

浏览器打印:

{ name: '阿狗' }
(2) 封闭对象:seal()

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。

var obj = {
   name : "阿猫",
   age : 20
}
Object.seal(obj);
obj.name = "阿狗";
delete obj.age;
obj.sex = "man";
console.log(obj);

浏览器打印:

{ name: '阿狗', age: 20 }
(3) 冻结对象: freeze()

Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。如下:

var obj = {
   name : "阿猫",
   age : 20
}
Object.freeze(obj);
obj.name = "阿狗";
delete obj.age;
obj.sex = "man";
console.log(obj);

浏览器打印:

{ name: '阿猫', age: 20 }

也可以使用isExtensible, isSealed, isFrozen来判断当处处理如个保护状态,如下:

1.Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

==只有这个返回值默认为true,因为这个方法是判断是否可以拓展,而默认的是可以==


var obj = {
    name : "阿猫",
    age : 20
}

console.log(Object.isExtensible(obj));
console.log(Object.isSealed(obj));
console.log(Object.isFrozen(obj));
Object.preventExtensions(obj)
console.log(Object.isExtensible(obj));
console.log(Object.isSealed(obj));
console.log(Object.isFrozen(obj));

浏览器打印如下:


true
false
false
{ name: '阿狗' }
false
false
false

2. Object.isSealed() 方法判断一个对象是否被密封。

var obj = {
    name : "阿猫",
    age : 20
}

Object.seal(obj)
console.log(Object.isExtensible(obj));
console.log(Object.isSealed(obj));
console.log(Object.isFrozen(obj));

浏览器打印如下:


{ name: '阿狗', age: 20 }
false
true
false

3. Object.isFrozen()方法判断一个对象是否被冻结。

var obj = {
    name : "阿猫",
    age : 20
}

Object.freeze(obj)
console.log(Object.isExtensible(obj));
console.log(Object.isSealed(obj));
console.log(Object.isFrozen(obj));

浏览器打印如下:


{ name: '阿猫', age: 20 }
false
true
true


5.2.4 Object.assign

Object.assign就去用于对象的合并,将源对象的所有的可枚举属性,复制到目标对象。

代码如下:


var obj1 = {};
var obj2 = {
    name:"阿猫",
    age:20
}
var obj3 = {
    name:"阿狗"
}

Object.assign(obj1,obj2,obj3)//obj1是目标对象,obj2、obj3都是源对象
console.log(obj1);

浏览器打印如下:


{ name: '阿狗', age: 20 }

==注意细节:==

  1. 第一个参数是目标对象,assign这个方法把其它的参数中的属性全部加在第一个参数身上。
  2. 第一个参数在属性复制过程中,可以会被修改,后面的会覆盖前面的属性
  3. assign这个方法的返回值就是第一个参数的引用,也就是返回了第一个参数。
  4. aassign这个方法会把原型上面的发展也拷贝了。
  5. assign不能拷贝继承过来的属性
  6. assign也不拷贝不可枚举的属性

如下:


var obj1 = {};
var obj2 = {
    name : "阿猫"
}
Object.defineProperty(obj2,"age",{
    configurable:true,
    writable:true,
    value:20,
    enumerable:false
})
Object.assign(obj1,obj2);
console.log(obj1);

浏览器打印如下:


{ name: '阿猫' }

  1. assign是浅复制,如下:

var obj1 = {};
var obj2 = {
    name:"阿猫",
    friends:["阿狗","阿郎"]
}
Object.assign(obj1,obj2);
console.log(obj1);
obj2.friends.push("阿毛");
console.log(obj1)


浏览器打印如下:


{ name: '阿猫', friends: [ '阿狗', '阿郎' ] }
{ name: '阿猫', friends: [ '阿狗', '阿郎', '阿毛' ] }

在对源对象的应用类型数据修改后,目标对象也修改了

5.3 对象扩展运算符

数组中的扩展运算符:


var arr1 = [1,2,3];
var arr2 = [...arr1]
console.log(arr2);

作用:用于取出参数对象的所有可遍历属性,拷贝当前对象中。


var obj1 =  {
    name : "阿猫",
    age : 20,
    friends : ["阿狗,阿毛"]
}

var obj2 = {...obj1}
console.log(obj2);
obj1.friends.push("阿郎")
console.log(obj2);

效果如下


{ name: '阿猫', age: 20, friends: [ '阿狗,阿毛' ] }
{ name: '阿猫', age: 20, friends: [ '阿狗,阿毛', '阿郎' ] }

还可以这样用:

var obj1 =  {
    name : "阿猫",
}

var obj2 = {
    age : 20
}
var obj3 = {...obj1,...obj2}
console.log(obj3);

打印如下:

{ name: '阿猫', age: 20 }

6. 函数的新增特性

6.1.1 函数参数的默认值

JavaScript函数的最大的一个特点就是在传递参数的时候,参数的个数不受限制的。为了健壮性考虑,一般在函数内部需要做一些默认值的处理。

function makeRequest(url, timeout, callback) {
    timeout = timeout || 2000;
    callback = callback || function() {};
}

其实上面的默认值方法有个bug:当timeout是0的时候也会当做假值来处理,从而给赋值默认值2000.

ES6从语言层面面上增加了 默认值的 支持。看下面的代码:

//这个函数如果只传入第一个参数,后面两个不传入,则会使用默认值。如果后面两个也传入了参数,则不会使用默认值。
function makeRequest(url, timeout = 2000, callback = function() {}) {

    // 其余代码

6.1.2 默认参数对 arguments 对象的影响

在非严格模式下,arguments总是能反映出命名参数的变化。看下面的代码:

    function foo(a, b) {
        //非严格模式
        console.log(arguments[0] === a); //true
        console.log(arguments[1] === b); //true
        a = 10;
        b = 20;
        console.log(arguments[0] === a); //true
        console.log(arguments[1] === b); //true
    }
    foo(1, 2);

在ES5的严格模式下,arguments只反映参数的初始值,而不再反映命名参数的变化!

    function foo(a, b) {
        //严格模式
        "use strict"
        console.log(arguments[0] === a); //true
        console.log(arguments[1] === b); //true
        a = 10;
        b = 20;
        console.log(arguments[0] === a); //false。  修改a的值不会影响到arguments[0]的值
        console.log(arguments[1] === b); //false
    }
    foo(1, 2);

当使用ES6参数默认值的时候,不管是否是在严格模式下,都和ES5的严格模式相同。看下面的代码:

    function foo(a, b = 30) {
        console.log(arguments[0] === a); //true
        console.log(arguments[1] === b); //true
        a = 10;
        b = 20;
        console.log(arguments[0]  === a); //false。  由于b使用了默认值。虽然a没有使用默认值,但是仍然表现的和严格模式一样。
        console.log(arguments[1] === b); //false。  b使用了默认值,所以表现的和严格模式一样。
    }
    foo(1, 2);

注意:如果这样调用foo(1),则 a == 1, b == 30, arguments[0] == 1, arguments[1] == undefined。也就是说默认值并不会赋值给arguments参数。

6.1.3 默认参数表达式 (Default Parameter Expressions)

参数的默认值,也可以是一个表达式或者函数调用等。看下面的代码

    function getValue() {
        return 5;
    }

    function add(first, second = getValue()) { //表示使用getValue这个函数的返回值作为second的默认值。
        return first + second;
    }

    console.log(add(1, 1));     // 2.  调用add函数的时候,传入了第二个参数,则以传入的参数为准。
    console.log(add(1));        // 6。 调用add函数的时候,没有传入第二个参数,则会调用getValue函数。

有一点需要要注意:getValue()只会在调用add且不传入第二个参数的时候才会去调用。不是在解析阶段调用的。

    let value = 5;
    function getValue() {
        return value++;
    }

    function add(first, second = getValue()) {  //
        return first + second;
    }

    console.log(add(1, 1));     // 2
    console.log(add(1));        // 6。 
    console.log(add(1));        // 7
    console.log(add(1));        // 8

由于默认值可以表达式,所以我们甚至可以使用前面的参数作为后面参数的默认值。

function add(first, second = first) {  // 使用第一个参数作为第二个参数的默认值
        return first + second;
 }

注意:可以把前面的参数作为后面参数的默认值,但是不能把后面的参数作为第一个参数的默认值。这可以前面说的let和const的暂存性死区一个意思。

function add(first = second, second)) {  // 这种写法是错误的

        return first + second;
}

6.2 箭头函数

ECMAScript 6 最有意思的部分之一就是箭头函数。正如其名,箭头函数由 “箭头”(=>)这种新的语法来定义。

其实在别的语言中早就有了这种语法结构,不过他们叫拉姆达表达式。

6.2.1 箭头函数语法

基本语法如下:

(形参列表)=>{
  //函数体
}

箭头函数可以赋值给变量,也可以像匿名函数一样直接作为参数传递。

示例1:

    var sum = (num1, num2) =>{
        return num1 + num2;
    }
    console.log(sum(3, 4));
    //前面的箭头函数等同于下面的传统函数
    var add = function (num1, num2) {
        return num1 + num2;
    }
    console.log(add(2, 4))

如果函数体内只有一行代码,则包裹函数体的 大括号 ({ })完全可以省略。如果有return,return关键字也可以省略。 如果函数体内有多条语句,则 {} 不能省略。

示例2:

    var sum = (num1, num2) => num1 + num2;
    console.log(sum(5, 4));
    //前面的箭头函数等同于下面的传统函数
    var add = function (num1, num2) {
        return num1 + num2;
    }
    console.log(add(2, 4));

    //如果这一行代码是没有返回值的,则方法的返回自也是undefined
    var foo = (num1, num2) => console.log("aaa");
    console.log(foo(3,4));  //这个地方的返回值就是undefined

如果箭头函数只有一个参数,则包裹参数的小括号可以省略。其余情况下都不可以省略。当然如果不传入参数也不可以省略

示例3:

    var foo = a=> a+3; //因为只有一个参数,所以()可以省略
    console.log(foo(4)); // 7

如果没有需要传入的参数,而且还不想添加传统的大括号和return,则必须给整个对象添加一个小括号 ()

示例4:

    var foo = ()=>({name:"lisi", age:30});
    console.log(foo());
    //等同于下面的;
    var foo1 = ()=>{
        return {
            name:"lisi",
            age : 30
        };
    }

6.2.2 使用箭头函数实现函数自执行

    var person = (name => {
            return {
                name: name,
                age: 30
            }
        }
    )("zs");
    console.log(person);

6.2.3 箭头函数中无this绑定

在ES5之前this的绑定是个比较麻烦的问题,稍不注意就达不到自己想要的效果。因为this的绑定和定义位置无关,只和调用方式有关。

在箭头函数中则没有这样的问题,在箭头函数中,this和定义时的作用域相关,不用考虑调用方式

箭头函数没有 this 绑定,意味着 this 只能通过查找作用域链来确定。如果箭头函数被另一个不包含箭头函数的函数囊括,那么 this 的值和该函数中的 this 相等,否则 this 的值为 window。

    var PageHandler = {
        id: "123456",
        init: function () {
            document.addEventListener("click",
                event => this.doSomething(event.type), false); // 在此处this的和init函数内的this相同。
        },

        doSomething: function (type) {
            console.log("Handling " + type + " for " + this.id);
        }
    };
    PageHandler.init();

看下面的一段代码:

    var p = {
        foo:()=>console.log(this)   //此处this为window
    }
    p.foo();  //输出为 window对象。   并不是我想要的。所以在定义对象的方法的时候应该避免使用箭头函数。
//箭头函数一般用在传递参数,或者在函数内部声明函数的时候使用。

说明:

箭头函数作为一个使用完就扔的函数,不能作为构造函数使用。也就是不能使用new 的方式来使用箭头函数。 由于箭头函数中的this与函数的作用域相关,所以不能使用call、apply、bind来重新绑定this。但是虽然this不能重新绑定,但是还是可以使用call和apply方法去执行箭头函数的。

6.2.4 无arguments绑定

虽然箭头函数没有自己的arguments对象,但是在箭头函数内部还是可以使用它外部函数的arguments对象的。

    function foo() {
        //这里的arguments是foo函数的arguments对象。箭头函数自己是没有 arguments 对象的。
        return ()=>arguments[0]; //箭头函数的返回值是foo函数的第一个参数
    }
    var arrow = foo(4, 5);
    console.log(arrow()); // 4

7. 新增数据结构

7.1 Set数据结构

set和数据差不多,也是一种集合,区别在于:它里面的值都是唯一的,没有重复的。

创建一个set对象如下

var setList = new Set();
    setList.add("a");
    setList.add("c");
    setList.add("b");
    setList.add("a");  //重复,所以添加失败。注意这个地方并不会保存。
    console.log(setList.size); // 长度是3

set的方法有add()、has()、delete()、clear()等方法,分别用于添加、查询、删除、和清空的功能。

var setList = new Set(["a","b","c","d"]);
console.log(setList);//{"a", "b", "c", "d"}
setList.add("e");
console.log(setList);//{"a", "b", "c", "d", "e"}
console.log(setList.has("b"));//true
setList.delete("c");
console.log(setList);//{"a", "b", "d", "e"}
setList.clear();
console.log(setList);//{}

使用set可以轻松的实现将一组数据去重

var arr1 = [1,1,2,2,3,3];
var arr2 = [...(new Set(arr1))]
console.log(arr2);//[1,2,3]

7.2 Map数据结构

它类似于对象,里面存放也是键值对,区别在于:对象中的键名只能是字符串,如果使用map,它里面的键可以是任意值。

创建一个map对象如下:

 var map = new Map();
    map.set("a", "lisi");
    map.set("b", "zhangsan");
    map.set("b", "zhangsan222");  // 第二次添加,新的value会替换掉旧的
    console.log(map.get("a"));
    console.log(map.get("b"));   //zhangsan222
    console.log(map.get("c")); //undefined.如果key不存在,则返回undefined
    console.log(map.size); //2

map中的方法有:

  1. has(key) - 判断给定的 key 是否在 map 中存在
  2. delete(key) - 移除 map 中的 key 及对应的值
  3. clear() - 移除 map 中所有的键值对
var map = new Map();
map.set("a","a");
map.set("b","b");
map.set("c","c");
console.log( map.get("a"))//a
console.log( map.has("a") ) //true
console.log( map.has("d") ) //false
map.clear();
console.log(map);//Map {}

8.Class

ES6从形式上,向主流的面向对象的语言靠拢,前面我们都是创建构造器,然后去new构造器,构造器就相当于一个类,在ES6中,就可以使用class来创建对象了。

8.1 class创建对象

上文说到,通过class创建对象的格式如下:

class 类名{
    constructor(参数){
        this.属性 = 参数;        
    }  
    running{//对象中简写方法,省略了function
        
    }
}

不使用Class,去创建一个类,如下:

function persion(name,age){
    this.name = name,
    this.age = age;
}

persion.prototype.say = function(){
    console.log(`我是${this.name},${this.age}岁`)
}

var p1 = new persion("阿毛",20);

在ES6中,可以使用class来声明一个类了,如下:


class Persion{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    say(){
        console.log(`${this.name}的年龄是${this.age}`)
    }
}

var p1 = new Persion("abc",12);

p1.say();

在浏览器中访问之,如下:


abc的年龄是12

虽然使用class非常给力,但是class是属性JS中高级的语法,可以转成低级的语法,如下Babel网站


8.2使用extends实现继承

格式:

class 子类 extends 父类{
    constructor(参数){
        super(参数);
        this.参数 = 值;
    }
}

注意:

  1. 使用 extends 关键字来实现继承
  2. 在子类中的构造器 constructor 中,必须要显式调用父类的 super 方法,如果不调用,则 this 不可用 在ES5的继承如下:
function Persion(name,age){
    this.name = name;
    this.age = age;
}
Persion.prototype.say = function(){
    console.log(`I'm ${this.name} and ${this.age}`);
}

function GoodMan(name,age,sex){
    //继承属性
    Persion.call(this,name,age);
    this.sex = sex;
}
//继承方法   利用浅拷贝继承方法
for (var p in Persion.prototype) {
    GoodMan.prototype[p] = Persion.prototype[p];
}
//GoodMan 自己的方法
GoodMan.prototype.do = function () {
    console.log(`I'm ${this.name},I'm ${this.sex}`);
}

var p1 = new GoodMan("阿毛",20,"男");
p1.do();
p1.say();

使用ES6中的extends来实现继承:


class Persion{
    constructor(name ,age){
        this.name = name;
        this.age = age;      
    }
    say(){
        console.log(`I'm ${this.name} and ${this.age}`);
    }
}

class GoodMan extends Persion{
    constructor(name,age,sex){
        super(name,age);
        this.sex = sex;
    }
    do(){
        console.log(`I'm ${this.name},I'm ${this.sex}`);
    }
}

p1 = new GoodMan("阿毛",20,"男");
p1.do();
p1.say();

在浏览器中效果如下:

I'm 阿毛,I'm 男
I'm 阿毛 and 20

8.3 类的静态方法 static

直接通过类名来访问的方法就是静态方法。如:Math.abs();这里的 abs()是静态方法。

class Persion{
   constructor(name,age){
       this.name = name;
       this.age = age;
   }
   say(){
       console.log(`${this.name}的年龄是${this.age}`)
   }
   static run(){
       console.log("running......")
   }
}
Persion.run();

调用时就是Persion.run();

==静态方法只能用类名调用!!!!==

静态方法也可以继承

   class Father{
       static foo(){
           console.log("我是父类的静态方法");
       }
   }
   class Son extends Father{

   }
   Son.foo(); //子类也继承了父类的静态方法。  这种方式调用和直接通过父类名调用时一样的。

总结:

整个ES6的新特性和用法学习下来,觉得比较有用的点如下:

  1. 优先使用let,因为相较var来说,let可以形成作用域,不易产生变量污染的情况。
  2. 不常变动的值使用const,方便阅读,也防止不小心的串改。
  3. 熟悉新加入的对字符串的处理函数,简化了对于字符串处理的过程。
  4. 使用模板字符串即 (`` ,不仅仅方便了字符串的整合,同时也省去样式的操作
  5. 使用匿名函数的场合,一律改为使用箭头函数。
  6. 增加Set和Map的使用率,可以减少仅仅使用Array和Object对象的时的一些复杂操作。

ES6新增加的内容还有很多,前端小白学习不够到位,还有很大的油田等待开采,文章中的内容有误之处,还请大家及时指正,小弟感激不尽。如有版权问题,请联系我