JavaScript ES6 核心功能一览

JavaScript ES6 核心功能一览(ES6 亦作 ECMAScript 6 或 ES2015+)

JavaScript 在过去几年里发生了很大的变化。这里介绍 12 个你马上就能用的新功能。

JavaScript 历史

新的语言规范被称作 ECMAScript 6。也称为 ES6 或 ES2015+ 。

自从 1995 年 JavaScript 诞生以来,它一直在缓慢地发展。每隔几年就会增加一些新内容。1997 年,ECMAScript 成为 JavaScript 语言实现的规范。它已经有了好几个版本,比如 ES3 , ES5 , ES6 等等。

如你所见,ES3,ES5 和 ES6 之间分别存在着 10 年和 6 年的间隔。像 ES6 那样一次进行大幅修改的模式被逐年渐进式的新模式所替代。

浏览器支持

所有现代浏览器和环境都已支持 ES6。

来源:https://kangax.github.io/compat-table/es6/

Chrome,MS Edge,Firefox,Safari,Node 和许多其他的环境都已内置支持大多数的 JavaScript ES6 功能。所以,在本教程中你学到的每个知识,你都可以马上开始应用。

让我们开始学习 ECMAScript 6 吧!

核心 ES6 功能

你可以在浏览器的控制台中测试所有下面的代码片段。

不要笃信我的话,而是要亲自去测试每一个 ES5 和 ES6 示例。让我们开始动手吧💪

变量的块级作用域

使用 ES6,声明变量我们可以用var,也可以用let或const。

var有什么不足?

使用var的问题是变量会漏入其他代码块中,诸如for循环或if代码块。

// ES5

var x = 'outer';

function test(inner) {

if (inner) {

var x = 'inner'; // 作用于整个 function

return x;

}

return x; // 因为第四行的声明提升,被重新定义

}

test(false); // undefined 😱

test(true); // inner

对于test(fasle),你期望返回outer,但是,你得到的是undefined。

为什么?

因为尽管没有执行if代码块,第四行中的表达式var x也会被提升。

var提升

var是函数作用域。在整个函数中甚至是声明语句之前都是可用的。

声明被提升。所以你能在声明之前使用一个变量。

初始化是不被提升的。如果你使用var声明变量,请总是将它放在顶部。

在应用了声明提升规则之后,我们就能更容易地理解发生了什么:

```

// ES5

var x = 'outer';

function test(inner) {

var x; // 声明提升

if (inner) {

x = 'inner'; // 初始化不被提升

return x;

}

return x;

}

```

ECMAScript 2015 找到了解决的办法:

// ES6

let x = 'outer';

function test(inner) {

if (inner) {

let x = 'inner';

return x;

}

return x; // 从第一行获取到预期结果

}

test(false); // outer

test(true); // inner

将var改为let,代码将像期望的那样运行。如果if代码块没有被调用,x变量也就不会在代码块外被提升。

let提升和“暂存死区(temporal dead zone)”

在 ES6 中,let将变量提升到代码块的顶部(不是像 ES5 那样的函数顶部)。

然而,代码块中,在变量声明之前引用它会导致ReferenceError错误。

let是块级作用域。你不能在它被声明之前引用它。

“暂存死区(Temporal dead zone)”是指从代码块开始直到变量被声明之间的区域。

IIFE

在解释 IIFE 之前让我们看一个例子。来看一下:

// ES5

{

var private = 1;

}

console.log(private); // 1

如你所见,private漏出(代码块)。你需要使用 IIFE(immediately-invoked function expression,立即执行函数表达式)来包含它:

// ES5

(function(){

var private2 = 1;

})();

console.log(private2); // Uncaught ReferenceError

如果你看一看 jQuery/loadsh 或其他开源项目,你会注意到他们用 IIFE 来避免污染全局环境而且只在全局中定义了诸如_,$和jQuery。

在 ES6 上则一目了然,我们可以只用代码块和let,也不再需要使用 IIFE了。

// ES6

{

let private3 = 1;

}

console.log(private3); // Uncaught ReferenceError

Const

如果你想要一个变量保持不变(常量),你也可以使用const。

总之:用let,const而不是var

对所有引用使用const;避免使用var。

如果你必须重新指定引用,用let替代const。

模板字面量

有了模板字面量,我们就不用做多余的嵌套拼接了。来看一下:

// ES5

var first = 'Adrian';

var last = 'Mejia';

console.log('Your name is ' + first + ' ' + last + '.');

现在你可以使用反引号 (`) 和字符串插值${}:

// ES6

const first = 'Adrian';

const last = 'Mejia';

console.log(`Your name is ${first} ${last}.`);

多行字符串

我们再也不需要添加 +\n来拼接字符串了:

// ES5

var template = '

\n' +

\n' +

'    \n' +

'    \n' +

'    \n' +

'  \n' +

'  \n' +

'';

console.log(template);

在 ES6 上, 我们可以同样使用反引号来解决这个问题:

// ES6

const template = `

  • `;

    console.log(template);

    两段代码的结果是完全一样的。

    解构赋值

    ES6 的解构不仅实用而且很简洁。如下例所示:

    从数组中获取元素

    // ES5

    var array = [1, 2, 3, 4];

    var first = array[0];

    var third = array[2];

    console.log(first, third); // 1 3

    等同于:

    const array = [1, 2, 3, 4];

    const [first, ,third] = array;

    console.log(first, third); // 1 3

    交换值

    // ES5

    var a = 1;

    var b = 2;

    var tmp = a;

    a = b;

    b = tmp;

    console.log(a, b); // 2 1

    等同于:

    // ES6

    let a = 1;

    let b = 2;

    [a, b] = [b, a];

    console.log(a, b); // 2 1

    多个返回值的解构

    // ES5

    function margin() {

    var left=1, right=2, top=3, bottom=4;

    return { left: left, right: right, top: top, bottom: bottom };

    }

    var data = margin();

    var left = data.left;

    var bottom = data.bottom;

    console.log(left, bottom); // 1 4

    在第 3 行中,你也可以用一个像这样的数组返回(同时省去了一些编码):

    return [left, right, top, bottom];

    但另一方面,调用者需要考虑返回数据的顺序。

    var left = data[0];

    var bottom = data[3];

    用 ES6,调用者只需选择他们需要的数据即可(第 6 行):

    // ES6

    function margin() {

    const left=1, right=2, top=3, bottom=4;

    return { left, right, top, bottom };

    }

    const { left, bottom } = margin();

    console.log(left, bottom); // 1 4

    注意:在第 3 行中,我们使用了一些其他的 ES6 功能。我们将{ left: left }简化到只有{ left }。与 ES5 版本相比,它变得如此简洁。酷不酷?

    参数匹配的解构

    // ES5

    var user = {firstName: 'Adrian', lastName: 'Mejia'};

    function getFullName(user) {

    var firstName = user.firstName;

    var lastName = user.lastName;

    return firstName + ' ' + lastName;

    }

    console.log(getFullName(user)); // Adrian Mejia

    等同于(但更简洁):

    // ES6

    const user = {firstName: 'Adrian', lastName: 'Mejia'};

    function getFullName({ firstName, lastName }) {

    return `${firstName} ${lastName}`;

    }

    console.log(getFullName(user)); // Adrian Mejia

    深度匹配

    // ES5

    function settings() {

    return { display: { color: 'red' }, keyboard: { layout: 'querty'} };

    }

    var tmp = settings();

    var displayColor = tmp.display.color;

    var keyboardLayout = tmp.keyboard.layout;

    console.log(displayColor, keyboardLayout); // red querty

    等同于(但更简洁):

    // ES6

    function settings() {

    return { display: { color: 'red' }, keyboard: { layout: 'querty'} };

    }

    const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();

    console.log(displayColor, keyboardLayout); // red querty

    这也称作对象的解构。

    如你所见,解构是非常实用的而且有利于促进良好的编码风格。

    最佳实践:

    使用数组解构去获取元素或交换值。它可以避免创建临时引用。

    不要对多个返回值使用数组解构,而是要用对象解构。

    类和对象

    用 ECMAScript 6,我们从“构造函数”🔨来到了“类”🍸。

    在 JavaScript 中,每个对象都有一个原型对象。所有的 JavaScript 对象都从它们的原型对象那里继承方法和属性。

    在 ES5 中,为了实现面向对象编程(OOP),我们使用构造函数来创建对象,如下:

    // ES5

    var Animal = (function () {

    function MyConstructor(name) {

    this.name = name;

    }

    MyConstructor.prototype.speak = function speak() {

    console.log(this.name + ' makes a noise.');

    };

    return MyConstructor;

    })();

    var animal = new Animal('animal');

    animal.speak(); // animal makes a noise.

    ES6 中有了一些语法糖。通过像class和constructor这样的关键字和减少样板代码,我们可以做到同样的事情。另外,speak()相对照constructor.prototype.speak = function ()更加清晰:

    // ES6

    class Animal {

    constructor(name) {

    this.name = name;

    }

    speak() {

    console.log(this.name + ' makes a noise.');

    }

    }

    const animal = new Animal('animal');

    animal.speak(); // animal makes a noise.

    正如你所见,两种式样(ES5 与 6)在幕后产生相同的结果而且用法一致。

    最佳实践:

    总是使用class语法并避免直接直接操纵prototype。为什么?因为它让代码更加简洁和易于理解。

    避免使用空的构造函数。如果没有指定,类有一个默认的构造函数。

    继承

    基于前面的Animal类。 让我们扩展它并定义一个Lion类。

    在 ES5 中,它更多的与原型继承有关。

    // ES5

    var Lion = (function () {

    function MyConstructor(name){

    Animal.call(this, name);

    }

    // 原型继承

    MyConstructor.prototype = Object.create(Animal.prototype);

    MyConstructor.prototype.constructor = Animal;

    MyConstructor.prototype.speak = function speak() {

    Animal.prototype.speak.call(this);

    console.log(this.name + ' roars 🦁');

    };

    return MyConstructor;

    })();

    var lion = new Lion('Simba');

    lion.speak(); // Simba makes a noise.

    // Simba roars.

    我不会重复所有的细节,但请注意:

    第 3 行中,我们添加参数显式调用了Animal构造函数。

    第 7-8 行,我们将Lion原型指派给Animal原型。

    第 11行中,我们调用了父类Animal的speak方法。

    在 ES6 中,我们有了新关键词extends和super

    // ES6

    class Lion extends Animal {

    speak() {

    super.speak();

    console.log(this.name + ' roars 🦁');

    }

    }

    const lion = new Lion('Simba');

    lion.speak(); // Simba makes a noise.

    // Simba roars.

    虽然 ES6 和 ES5 的代码作用一致,但是 ES6 的代码显得更易读。更胜一筹!

    最佳实践:

    使用extends内置方法实现继承。

    原生 Promises

    从回调地狱👹到 promises🙏。

    // ES5

    function printAfterTimeout(string, timeout, done){

    setTimeout(function(){

    done(string);

    }, timeout);

    }

    printAfterTimeout('Hello ', 2e3, function(result){

    console.log(result);

    // 嵌套回调

    printAfterTimeout(result + 'Reader', 2e3, function(result){

    console.log(result);

    });

    });

    我们有一个接收一个回调的函数,当done时执行。我们必须一个接一个地执行它两次。这也是为什么我们在回调中第二次调用printAfterTimeout的原因。

    如果你需要第 3 次或第 4 次回调,可能很快就会变得混乱。来看看我们用 promises 的写法:

    // ES6

    function printAfterTimeout(string, timeout){

    return new Promise((resolve, reject) => {

    setTimeout(function(){

    resolve(string);

    }, timeout);

    });

    }

    printAfterTimeout('Hello ', 2e3).then((result) => {

    console.log(result);

    return printAfterTimeout(result + 'Reader', 2e3);

    }).then((result) => {

    console.log(result);

    });

    如你所见,使用 promises 我们能在函数完成后进行一些操作。不再需要嵌套函数。

    箭头函数

    ES6 没有移除函数表达式,但是新增了一种,叫做箭头函数。

    在 ES5 中,对于this我们有一些问题:

    // ES5

    var _this = this; // 保持一个引用

    $('.btn').click(function(event){

    _this.sendData(); // 引用的是外层的 this

    });

    $('.input').on('change',function(event){

    this.sendData(); // 引用的是外层的 this

    }.bind(this)); // 绑定到外层的 this

    你需要使用一个临时的this在函数内部进行引用或用bind绑定。在 ES6 中,你可以用箭头函数。

    // ES6

    // 引用的是外部的那个 this

    $('.btn').click((event) =>  this.sendData());

    // 隐式返回

    const ids = [291, 288, 984];

    const messages = ids.map(value => `ID is ${value}`);

    For…of

    从for到forEach再到for...of:

    // ES5

    // for

    var array = ['a', 'b', 'c', 'd'];

    for (var i = 0; i < array.length; i++) {

    var element = array[i];

    console.log(element);

    }

    // forEach

    array.forEach(function (element) {

    console.log(element);

    });

    ES6 的 for…of 同样可以实现迭代。

    // ES6

    // for ...of

    const array = ['a', 'b', 'c', 'd'];

    for (const element of array) {

    console.log(element);

    }

    默认参数

    从检查一个变量是否被定义到重新指定一个值再到default parameters。 你以前写过类似这样的代码吗?

    // ES5

    function point(x, y, isFlag){

    x = x || 0;

    y = y || -1;

    isFlag = isFlag || true;

    console.log(x,y, isFlag);

    }

    point(0, 0) // 0 -1 true 😱

    point(0, 0, false) // 0 -1 true 😱😱

    point(1) // 1 -1 true

    point() // 0 -1 true

    可能有过,这是一种检查变量是否赋值的常见模式,不然则分配一个默认值。然而,这里有一些问题:

    第 8 行中,我们传入0, 0返回了0, -1。

    第 9 行中, 我们传入false但是返回了true。

    如果你传入一个布尔值作为默认参数或将值设置为 0,它不能正常起作用。你知道为什么吗?在讲完 ES6 示例后我会告诉你。

    用 ES6,现在你可以用更少的代码做到更好!

    // ES6

    function point(x = 0, y = -1, isFlag = true){

    console.log(x,y, isFlag);

    }

    point(0, 0) // 0 0 true

    point(0, 0, false) // 0 0 false

    point(1) // 1 -1 true

    point() // 0 -1 true

    请注意第 5 行和第 6 行,我们得到了预期的结果。ES5 示例则无效。首先检查是否等于undefined,因为false,null,undefined和0都是假值,我们可以避开这些数字,

    // ES5

    function point(x, y, isFlag){

    x = x || 0;

    y = typeof(y) === 'undefined' ? -1 : y;

    isFlag = typeof(isFlag) === 'undefined' ? true : isFlag;

    console.log(x,y, isFlag);

    }

    point(0, 0) // 0 0 true

    point(0, 0, false) // 0 0 false

    point(1) // 1 -1 true

    point() // 0 -1 true

    当我们检查是否为undefined后,获得了期望的结果。

    剩余参数

    从参数到剩余参数和扩展操作符。

    在 ES5 中,获取任意数量的参数是非常麻烦的:

    // ES5

    function printf(format) {

    var params = [].slice.call(arguments, 1);

    console.log('params: ', params);

    console.log('format: ', format);

    }

    printf('%s %d %.2f', 'adrian', 321, Math.PI);

    我们可以用 rest 操作符...做到同样的事情。

    // ES6

    function printf(format, ...params) {

    console.log('params: ', params);

    console.log('format: ', format);

    }

    printf('%s %d %.2f', 'adrian', 321, Math.PI);

    展开运算符

    从apply()到展开运算符。我们同样用...来解决:

    提醒:我们使用apply()将数组转换为一列参数。例如,Math.max()作用于一列参数,但是如果我们有一个数组,我们就能用apply让它生效。

    正如我们较早之前看过的,我们可以使用apply将数组作为参数列表传递:

    // ES5

    Math.max.apply(Math, [2,100,1,6,43]) // 100

    在 ES6 中,你可以用展开运算符:

    // ES6

    Math.max(...[2,100,1,6,43]) // 100

    同样,从concat数组到使用展开运算符:

    // ES5

    var array1 = [2,100,1,6,43];

    var array2 = ['a', 'b', 'c', 'd'];

    var array3 = [false, true, null, undefined];

    console.log(array1.concat(array2, array3));

    在 ES6 中,你可以用展开运算符来压平嵌套:

    // ES6

    const array1 = [2,100,1,6,43];

    const array2 = ['a', 'b', 'c', 'd'];

    const array3 = [false, true, null, undefined];

    console.log([...array1, ...array2, ...array3]);

    总结

    JavaScript 经历了相当多的修改。这篇文章涵盖了每个 JavaScript 开发者都应该了解的大多数核心功能。同样,我们也介绍了一些让你的代码更加简洁,易于理解的最佳实践。

  • 最后编辑于
    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 194,761评论 5 460
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 81,953评论 2 371
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 141,998评论 0 320
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 52,248评论 1 263
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 61,130评论 4 356
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 46,145评论 1 272
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 36,550评论 3 381
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 35,236评论 0 253
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 39,510评论 1 291
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 34,601评论 2 310
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 36,376评论 1 326
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 32,247评论 3 313
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 37,613评论 3 299
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 28,911评论 0 17
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 30,191评论 1 250
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 41,532评论 2 342
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 40,739评论 2 335

    推荐阅读更多精彩内容