首页>前端教程>JavaScript教程

JavaScript(ES6)的常用特性!(上)

这个版本算是JavaScript划时代的一个升级了,从ES3、ES5、ES6,为什么没有ES4,其实ES5和ES6加起来就是曾经因为跨度太大导致流产的ES4.

这个版本升级的力度很大,有了很多新东西,需要好好掌握。

1、ES6介绍

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

2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。

因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

2、块作用域声明

在ES6以前,只有全局和函数局部作用域,需要创建局部作用域,最普通的方法除了普通的函数声明之外,就是立即调用函数表达式(IIFE)。

var a = 10;
(function IIFE(){
    var a = 20;
    console.log(a);
})();
console.log(a);

2.1 let声明

在ES6中,只需要一对{}就可以创建一个块作用域。

{
    let a = 2;
    if(a > 1){
        let b = a * 3;
        console.log(b); // 6
        for(let i = a; i <= b; i++){
            let j = i + 10;
            console.log(j);
        }
        let c = a + b;
        console.log(c);
    }
}
思考:哪些变量只存在于if语句内部?哪些变量只存在于for循环内部?

2.1.1 var变量提升,let变量暂时性死区

{
    console.log(a); //undefined
    console.log(b); //ReferenceError 临时死亡区(TDZ)
    var a;
    let b;
}

不管是否显性赋值,都不能在let b语句运行之前访问b。

对于TDZ值和未声明值(以及声明过的),typeof结果是不相同的。

{
    //a未声明
    if(typeof a === 'undefined'){
        console.log('变量没有声明');
    }
    //b声明了,但是还处于TDZ
    if(typeof b === 'undefined'){
        //ReferenceError
    }
    let b;
}

这里a是未声明的,所以typeof是检查它是否存在的唯一安全的方法,而typeof b 会抛出TDZ错误,因为代码后面有一个let b 声明。

2.1.2 let + for

var funcs = [];
for (var i = 0; i < 5; i++) {
      funcs.push(function () {
          console.log(i);
      });
}
funcs[0](); // 5
funcs[1](); // 5

//let 方法

var funcs = [];
for (let i = 0; i < 5; i++) {
      funcs.push(function () {
          console.log(i);
      });
}
funcs[0](); // 0
funcs[1](); // 1

for循环头部的let i 不只为for循环本身声明了一个i,而是为循环的每一次迭代都重新声明了一个新的i。这意味着循环迭代的函数会封闭一个新的i。

等同于如下代码的功能:

var funcs = [];
for (var i = 0; i < 5; i++) {
    let j = i;
      funcs.push(function () {
          console.log(j);
      });
}
funcs[0](); // 0
funcs[1](); // 1

let放在for...in和for...of循环中也是一样的。

2.2 const声明

用于创建常量。

常量是一个设定了初始值之后就只读的变量。

这个变量的值在声明时设定之后就不允许改变。const声明必须要有显式的初始值。

const PI = 3.14;
const a = undefined; // 如果需要一个值为undefined的常量,就要声明并初始化。
console.log(PI);// 3.14
PI = 3.1415926; // TypeError

常量不是对这个值本身的限制,而是对赋值的那个变量的限制。

这个值并没有因为const被锁定或者不可变,只是赋值本身不可变。

如果这个值是复杂值,比如数组或者对象,其内容仍然是可以修改的。

const a = [1,2];
a.push(3);
console.log(a); // [1,2,3]
a = 10; // TypeError

变量a持有一个指向数组的地址引用,数组本身是可以改变的,只是这个地址引用不可以变化。

将一个对象或数组作为常量赋值,意味着这个值在这个常量的词法作用域结束之前不会被垃圾回收,因为指向这个值的引用没有清除。所以如果不想出现这种情况,最好不要用常量指向复杂数据类型。

2.3 块作用域函数

从ES6开始,块内声明的函数,其作用域在这个块内。

{
    foo(); //可以调用
    function foo(){
        // ...
    }
}
foo(); // ReferenceError

要注意以下这种行为,在ES6中会报错

if(something){
    function foo(){
        console.log('1');
    }
}else{
    function foo(){
        console.log('2');
    } 
}
foo(); // 在ES6以前,不管something是什么,foo()都会打印2,因为两个函数声明都会被提升到块外面,第二个总是会覆盖前面的函数声明。在ES6中,最后一行会抛出ReferenceError

3、spread/rest

ES6引入的新运算符...,通常称为spread或rest(展开或收集)运算符,取决于它在哪里如何使用。

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

let arr = [1,2,3];
// 老方法,用apply实现,数组可以作为参数。
// let oNull = Object.create(null);
// console.log(Math.max.apply(oNull,arr));
// 当...用在可以被遍历的对象前,比如数组,它会把这个变量展开spread为各个独立的值。
let max = Math.max(...arr);
console.log(max);

还可以在其他上下文中用来展开一个值,比如:

let a = [1,2,3];
let b = ['a',...a,'b'];
console.log(b); //['a', 1, 2, 3, 'b']
// 老方法,用concat合并数组。
console.log(['a'].concat(a,'b'));

除了spread展开之外,还可以反向操作,把一系列值收集到一起成为一个数组。

//z表示:把剩下的参数收集到一起组成一个名为z的数组
function foo(x,y,...z){
    console.log(x,y,z);
}
foo(1,2,3,4,5); // 1 2 [3,4,5]

如果没有命名参数的话,就会收集所有的参数:

//这里的...表示收集,可以叫做rest参数,表示收集其余的参数。
function foo(...args){
    // 老方法,获得除第一个后面所有的参数
    console.log(Array.prototype.slice.call(arguments,1));
    // 新方法
    console.log(args.slice(1));   
    console.log(args); //[1, 2, 3, 4]
    //这里的...表示扩展,把数组扩展成单个的值。这里很好的展示了...运算符对称而又相反的用法。
    console.log(...args); //1 2 3 4
}
foo(1,2,3,4);

4、默认参数值

当函数的参数可以缺省的时候,需要设置一个默认的值。

function foo(x, y) {
    //这种默认值设置,当参数可以为0的时候会出错。
    x = x || 10;
    y = y || 20;
    return x + y;
}
console.log(foo());
// 当x为0的时候,会出错,因为 0 || 10 ,会返回10
console.log(foo(0, 10));
console.log(foo(10))
// 修正参数可以为0的时候,也意味着除了undefined之外的任何值都可以传入。
// 如果传入undefined,在这里还是表示该参数缺省。
function foo(x, y) {
    x = (x !== undefined) ? x : 10;
    y = (y !== undefined) ? y : 20;
    return x + y;
}
console.log(foo());
console.log(foo(0, 10));
console.log(foo(10));
// 表示第一个参数缺省。
console.log(foo(undefined,10)); // 20
//但是如果参数需要传递undefined ,可以通过它不存在于arguments中来确定这个参数是否被省略。
function foo(x,y){
    console.log(arguments);
    // 这里 in 表示属性是否存在于对象中,0表示的是下标。
    x = (0 in arguments) ? x : 10;
    y = (1 in arguments) ? y : 20;
    return x + y;
}
console.log(foo()); // 30
console.log(foo(10,undefined)); // NaN
// 但是如果不能传递任何值,甚至undefined也不行,来表明“我省略了这个参数”,那么如何省略第一个参数呢?
// foo(,10)这是不合法的语法,而在函数参数中,undefined和缺失是无法区分的
// 所以,只能缺省右侧的参数,而不能省略位于参数列表中间或者起始处的参数。
// ES6新增了一个有用的语法来改进为缺失参数赋默认值的流程。
// 这里的x = 10更像是x !== undefined ? x : 10
function foo(x = 10,y = 20){
    return x + y;
}
console.log(foo()); // 30
console.log(foo(5)); // 25
console.log(foo(0,10)); // 10
console.log(foo(5,undefined)); // 25 ,丢掉undefined,用默认值
console.log(foo(5,null)); // null转成了0
console.log(foo(undefined,5)); // 15 ,undefined转成默认值。
console.log(foo(null,5)); // 5
console.log(foo(null,undefined)); // 20
console.log(foo('',undefined)); // '20'
console.log(foo(' ',undefined)); // ' 20'
console.log(foo(true,undefined)); // 21 true被强制转成1
console.log(foo(true,false)); // 1 true被强制转成1,false强制转成0

5、解构

ES6引入了一个新的语法特性,名为解构(destructuring),把这个功能看作是一个结构化赋值(structured assignment)方法。

以前的手动赋值方法:

function foo() {
    return [1, 2, 3];
}
let temp = foo();
//手动赋值
let a = temp[0], b = temp[1], c = temp[2];
console.log(a, b, c);

function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let tmp = bar();
//手动赋值
let x = tmp.x, y = tmp.y, z = tmp.z;
console.log(x, y, z)

可以把将数组或者对象属性中带索引的值手动赋值看做结构化赋值。

ES6为解构新增了一个专门语法,专用于数组解构对象解构

这个语法消除了前面代码中对临时变量tmp的需求,使代码简洁更多。

function foo() {
    return [1, 2, 3];
}
function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let [a, b, c] = foo();
let { x: x, y: y, z: z } = bar();
console.log(a, b, c);
console.log(x, y, z);

5.1 对象属性赋值模式

当对象的属性名和要赋值的变量名相同,语法还可以更简洁一些:

function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let { x,y,z } = bar();
console.log(x, y, z);

省略了x:这个部分,如果有需要把属性赋值给非同名变量,则:

function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let { x: bam, y: baz, z: bap } = bar();
console.log(bam,baz,bap);

在对象字面量或者常规的赋值语句中,都是target:source模式,或者说是:property-alias( 属性别名 ):value模式。

但是在对象解构赋值的时候,反转了target:source模式。变成了source:target。

let aa = 10; bb = 20;
// 对象字面量是target:source模式
let o = { x: aa, y: bb };
// 对象解构赋值是source:target模式
// 如果省略了x: ,相当于把aa的值赋值给了AA,bb的值赋值给了BB。
// 对称性可以帮助解释为什么ES6这个语法模式进行了反转,虽然脑子有点不习惯。
let { x: AA, y: BB } = o;
console.log(AA, BB); // 10 20

可以不用临时变量解决“交换两个变量”这个经典问题:

let x = 10, y = 20;
// 老方法
// let temp = 0;
// temp = x;
// x = y;
// y = temp;
// console.log(x,y); // 20 10
// 新方法
[y,x] = [x,y];
console.log(x,y); // 20 10

5.2 重复赋值

对象解构形式允许多次列出同一个源属性。

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

5.3 缺省

对于数组解构赋值和对象解构赋值,不需要把存在的所有值都用来赋值。

function foo() {
    return [1, 2, 3];
}
function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
// 因为数组是按照顺序解构的,所以第一个值不要的时候,还是要有占位。
let [,b] = foo();
let {x,z} = bar();
console.log(b,x,z); // 2 4 6

当然,如果超过了可以解构的值,多余的值会被赋为undefined.

function foo() {
    return [1, 2, 3];
}
function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let [,,c,d] = foo();
let {w,z} = bar();
console.log(c,d,w,z); // 3 undefined undefined 6

这个特性也是符合“undefined就是缺失”的原则。

...运算符也可以执行解构赋值同样的操作:

function foo() {
    return [1, 2, 3];
}
// ...b收集了其余的值,组成一个数组。
// 在ES6中还没有通过...展开和集合对象的特性。
let [a,...b] = foo();
console.log(a,b); //1 [2, 3]

5.4 默认值解构

使用和前面函数参数默认值类似的=语法,解构也可以提供一个用来赋值的默认值。

function foo() {
    return [1, 2, 3];
}
function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let [a = 0, b = 0, c = 0, d = 4] = foo();
console.log(a, b, c, d); // 1 2 3 4

let { x = 0, y = 0, z = 0, w = 10 } = bar();
console.log(x, y, z, w); // 4 5 6 10

或者,属性名和变量名不同的时候:

function bar() {
    return {
        x: 4,
        y: 5,
        z: 6
    };
}
let { x, y, z, w: WW = 20 } = bar();
console.log(x, y, z, WW); //4 5 6 20

5.5 嵌套解构

如果解构的值中有嵌套的对象或者数组,也可以解构这些嵌套的值:

let a1 = [1, [2, 3, 4], 5];
let o1 = { x: { y: { z: 6 } } };
let [a, [b, c, d], e] = a1;
let { x: { y: { z: w } } } = o1;
console.log(a,b,c,d,e); // 1 2 3 4 5
console.log(w); // 6

把嵌套解构当作一种展平对象名字空间的简单方法 .

let App = {
    model: {
        User: function () {//...}
        }
    }

}
// 老方法
let User = App.model.User;
// 新方法
let { model: { User } } = App;
const APP = {
    methods: {
        say1() {
            return 'hello';
        },
        say2() {
            return 'world';
        }
    }

}
console.log(APP.methods.say1());
console.log(APP.methods.say2());

// const say1 = APP.methods.say1;
// const say2 = APP.methods.say2;
// console.log(say1());
// console.log(say2());

const { methods: { say1 }, methods: { say2 } } = APP;
console.log(say1());
console.log(say2());
const APP = {
    say1() {
        return 'hello';
    },
    say2() {
        return 'world';
    }
}
const { say1, say2 } = APP;
console.log(say1());
console.log(say2());

6、对象字面量扩展

ES6为普通{}对象字面量新增了几个重要的便利扩展。

6.1 简洁属性

var x = 2,y = 3,
    o = {
        x:x,
        y:y
    }

当需要定义一个与某个标识符同名的属性的时候,可以把x:x简写成x。

var x = 2,y = 3,
    o = {
        x,
        y
    }

6.2 简洁方法

关联到对象字面量属性上的函数也有简洁形式。

var o = {
    x:function(){
        //...
    },
    y:function(){
        //...
    }
}

在ES6中可以简写:

var o = {
    x(){
        //...
    },
    y(){
        //...
    }
}

这种简洁方式都是使用了匿名函数表达式,所以针对需要递归,需要函数的词法标识符的时候就不可取了。

const obj = {
    getSum: function getSum(num) {
        if (num === 1) {
            return 1;
        }
        return num + getSum(num - 1);
    }
}
console.log(obj.getSum(5))

比如下面的这段代码:产生两个随机数字,然后用大的减去小的。

function run(o) {
    var x = getRandom(0, 100);
    var y = getRandom(0, 100);
    console.log(`x:${x},y:${y}`);
    // console.log(o.calc.name);
    //通过o.属性名来调用函数,是函数的公开名称
    return o.calc(x, y);
}
let result = run({
    calc: function calc(x, y) {
        if (x > y) {
        //递归,调用函数本身,calc是函数自身的词法标识符,不是对象的属性名称。用于在其自身内部递归调用函数。
            return calc(y, x);
        }
        return y - x;
    }

})
function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}
console.log(result);

如果把上面的代码进行ES6简洁:

let result = run({
    calc(x, y) {
        if (x > y) {
            //这种ES6的简洁形式,失去了函数本身的词法标识符,就不能递归调用自身了。会报ReferenceError错误。
            return calc(y, x);
        }
        return y - x;
    }
})

这段ES6的代码会被解释成:

let result = run({
    calc: function (x, y) {
        if (x > y) {
            //这时候calc作为属性就不能这样使用了。
            return calc(y, x);
        }
        return y - x;
    }
})

所以对于简洁方法应该只在不需要它们执行递归或者事件绑定/解绑定的时候使用。否则的话,还是按照老式的calc:function calc(...)方法来定义吧。

6.3 Getter/Setter

曾经的getter和setter的写法:

//  曾经的getter setter

let obj = {
    id: 10,
}
Object.defineProperty(obj, 'id', {
    get: function () {
        return this._id ++;
    },
    set: function (val) {
        this._id = val;
    }
})
console.log(obj.id); // NaN   undefined++ => NaN
obj.id = 12;
console.log(obj.id); // 12
console.log(obj.id); // 13
console.log(obj.id); // 14

简写形式:

let obj = {
    id: 10,
    get id() {
        return this._id++;
    },
    set id(val) {
        this._id = val;
    }
}

console.log(obj.id); // NaN
obj.id = 12;
console.log(obj.id); // 12
console.log(obj.id); // 13
console.log(obj.id); // 14

7、插入字符串字面量(模板字面量)

使用`作为界定符,这样的字符串字面值支持嵌入基本的字符串插入表达式,会被自动解析和求值。

let name = '诸葛亮';
// 用``来包围,会被解释为一个字符串字面量,其中任何${}形式的表达式都会被立即在线解析求值。
// 这种形式的解析求值就是插入(比模板要精确一些)
let greeting = `Hello,${name}`;
console.log(greeting)

插入字符串字面量的一个优点就是可以分散在多行:

let name = '诸葛亮';
// 用``来包围,会被解释为一个字符串字面量,其中任何${}形式的表达式都会被立即在线解析求值。
// 这种形式的解析求值就是插入(比模板要精确一些)
let greeting = `Hello,${name},
你是我的偶像,羽扇纶巾,运筹帷幄之中,决胜千里之外,鞠躬尽瘁,死而后已!`;
console.log(greeting)

7.1 插入表达式

在${...}内可以出现任何合法的表达式,包括函数调用、在线函数表达式调用、设置其它插入字符串字面量

function getUpper(str){
    return str.toUpperCase();
}

let name = 'daisy';
let str = `Hello,${getUpper(name)}`;
console.log(str)

8、箭头函数

理解使用普通函数基于this带来的困扰,这是新的ES6箭头函数=>特性引入的主要动因。

箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用(...)包围起来),然后是标识=>,函数体放在最后。

function foo(x,y){
    return x + y;
}
//箭头函数
var foo = (x,y) => x + y;

只有在函数体的表达式个数多于1个,或者函数体包含非表达式语句的时候才需要用{...}包围。

如果只有一个表达式,并且省略了包围的{...}的话,则意味着表达式前面有一个隐含的return。

var fn1 = () => 12;
var fn2 = x => x * 12;
var fn3 = (x,y) => {
    var z = x * 2 + y;
    y++;
    x *= 3;
    return (x + y + z) / 2
}

箭头函数总是函数表达式,并不存在箭头函数声明。

箭头函数是匿名函数表达式,它们没有用于递归或者事件绑定/解绑定的命名引用。

箭头函数支持普通函数参数的所有功能,包括默认值、解构、rest参数等。

箭头函数的简洁,使得它很适合这种情况:

var a = [1,2];
var result = a.map(v => v * 2);

但是=>箭头函数带来的可读性提升与被转化函数的长度负相关。

函数越长,箭头函数带来的好处就越小,函数越短,箭头函数带来的好处就越大。

所以,更合理的做法是只在确实需要简短的在线函数表达式的时候才采用箭头函数,而对于那些一般长度的函数则无需改变。

不只是更短的语法,而是this。

箭头函数的主要设计目的就是以特定的方式改变this的行为特性,解决this相关的一个特殊而又常见的痛点。

const obj = {
    name: '诸葛亮',
    fn() {
        console.log(this.name);
        //针对定时器的this
        setTimeout(() => {
            console.log(this.name);
        }, 1000)
    }
}

obj.fn();
// 点击按钮,每次加一个1
const oBtn = document.querySelector('button');
var controller = {
    makeRequest: function (num) {
        oBtn.innerText = num;
        num++;
        // 保存this对象
        var _this = this;
        oBtn.onclick = function () {
            // 当有点击事件的调试,让程序在这里进入调试
            // debugger;
            // console.log(this); //oBtn
            // controller.makeRequest(num); //这种方式不安全,有可能对象和标识符的引用关系发生了变化。
            // 通过闭包访问外层函数的变量_this
            _this.makeRequest(num);
        }
    }
}
controller.makeRequest(10);

在箭头函数内部,this绑定不是动态的,而是词法的。

var controller = {
    makeRequest: function (num) {
        oBtn.innerText = num;
        num++;
        oBtn.onclick = () => {
            // 箭头函数内部的this指向外层环境的this。这是词法绑定,这时的this不是动态的。
            this.makeRequest(num);
        }
    }
}

如果想灵活运用this,就不要胡乱使用箭头函数,比如:

var obj = {
    firstName: '诸葛',
    lastName: '孔明',
    fullName() {
        console.log(this)
        return this.firstName + this.lastName;
    },
    sayName() {
        return `Hi, my name is ${this.fullName()}`
    }
}

console.log(window.obj.fullName())
console.log(window.obj.sayName())
var controller = {
    //箭头函数里面的this从包围的作用域中词法继承而来。外围的作用域是全局作用域。
    makeRequest: () => {
        console.log(this);//window
        this.helper(); //报错
    },
    helper: () => {
        console.log(this);
    }
}
//虽然是对象.方法的隐式this绑定方式,但是makeRequest方法是箭头函数,它里面的this不指向这个对象了。
controller.makeRequest();

如果有一个简短单句在线函数表达式,其中唯一的语句是return某个计算出的值,且这个函数内部没有this引用,且没有自身引用(递归、事件绑定/解绑定),且不会要求函数执行这些,那么可以安全地把它重构成箭头函数。

如果有一个内层函数表达式,依赖于在包含它的函数中调用var _this = this或者.bind(this)来确保适当的this绑定,那么这个内层函数表达式应该可以安全地转换为箭头函数。

如果内层函数表达式依赖于封装函数中某种像var args = Array.prototype.slice.call(arguments)来保证arguments的词法复制,那么这个内层函数可以安全转成箭头函数。

所有的其它情况,函数声明、较长的多语句函数表达式、需要词法名称标识符(递归)的函数,以及任何不符合以上几点特征的函数,一般都应该避免箭头函数语句。

点赞


2
保存到:

相关文章

发表评论:

◎请发表你卖萌撒娇或一针见血的评论,严禁小广告。

Top