ES6小技巧在项目实践中的实用性分析-射手猫的个人博客

首先这篇文章,绝大部分转自掘金的翻译[翻译] 让你的代码更简短,更整洁,更易读的ES6小技巧。这篇文章仅仅是自己对掘金文章的笔记,对原文章进行了部分标记,并加入了部分自己的理解和实践延伸。

这篇文章有的知识点是自己知道的,其中在实际工作中用得比较多,但有的却几乎不怎么用,读后才有种原来你是这个时候派上用场的感觉,但是还是会忍不住思考在实际项目的实用性。

按照个人习惯,首先是所有知识点的总览:

1,模板字符串(推荐!)

2,块级作用域语法(推荐!)

3,扩展运算符(合并数组中的每项时,推荐使用!)

4,参数默认值(不是很推荐)

5,解构(不是很推荐)

6,对象字面量和属性的简洁表达式(不是很推荐)

7,动态属性名称(按实际需求,推荐使用)

8,箭头函数(推荐!)

9,for...of loops(for...of循环)  (按实际需求,推荐使用)

10,数字字面量 (按实际需求,推荐使用)

 

1,模板字符串

模板字符串使字符串的使用变得比以前更简单了,他们以反引号开始(`),并且能过使用${变量}来插入变量。我们来比较一下下面两行代码。

var fName = 'Peter', sName = 'Smith', age = 43, job= 'photographer';
var a = 'Hi, I\'m ' + fName + ' ' + sName + ', I\'m ' + age + ' and work as a ' + job + '.';
var b = `Hi, I'm ${ fName } ${ sName }, I'm ${ age } and work as a ${ job }.`;

一切都变得很美好了是不是,代码更易读了是不是?你可以在大括号内放入任何东西:变量,等式,或者函数的调用。 我将会在后面的整个文章的示例中使用这些方式。

个人评价:这个功能是实际用得灰常多的,基本上用字符串的地方就会用到,尤其是ajax的url。用法也是很简单,就是将反引号`(数字1的左边那个)当作以前的引号来用,以前用+连接的变量,换成${变量}的形式。

2,块级作用域

JavaScript是使用函数作用域的,这就是为什么我们是为什么我们越来越频繁的使用匿名的立即执行函数表达式(iife)来实现整个JavaScript文件的封装。我们这么做是为了把所有的变量隔离在文件内从而避免变量冲突。

现在我们有了块级作用域和两个崭新的块级作用域的变量声明

let declaration let命令

这个命令和var很相似但却又有着显著的不同。因为他是有块级作用域的,声明一个相同名字的新变量可以完全不影响外部的变量。

var a = 'car' ;
{
    let a = 5;
    console.log(a) // 5
}
console.log(a) // car

因为他是被限制在块级作用域的,他解决了那道非常经典的面试题:“下面这个代码的输出是什么,如何修改让他运行之后成为你想的那个样子?”

for (var i = 1; i < 5; i++){
    setTimeout(() => { console.log(i); }, 1000);
}

这个例子中,输出是“5 5 5 5 5”因为变量i在每次迭代中都会改变。

如果我们把var变为let,一切都变了。 现在,每次循环都会创建一个全新的块级作用域吧i限制在当前的循环,他可以理解为这样:

{let i = 1; setTimeout(() => { console.log(i) }, 1000)} 
{let i = 2; setTimeout(() => { console.log(i) }, 1000)} 
{let i = 3; setTimeout(() => { console.log(i) }, 1000)} 
{let i = 4; setTimeout(() => { console.log(i) }, 1000)} 
{let i = 5; setTimeout(() => { console.log(i) }, 1000)} 

varlet的另外一个区别是 let 不会像 var一样被变量提升

{ 
    console.log(a); // undefined
    console.log(b); // ReferenceError
    var a = 'car';
    let b = 5;
}

因为他有更为局限的作用域,以及更能被预测的行为,因此一些人甚至认为你应该使用let来代替var, 除非当你真的特别需要变量提升或者更宽松的作用域范围,你再使用var

Const

在以前,如果你想在JavaScript中声明一个常量, 习惯性的做法是使用全大写来命名。然鹅,这不是真的去保护了这个变量不能被更改---只是让其他的开发者知道,这是一个常量,它不应该被更改。

现在我们有了const命令.

const没有让变量完全不可变,只是锁定他的赋值,当你有一个复杂的变量(数组或者对象)的时候,值还是可以被修改的。

{
    const d = [1, 2, 3, 4];
    const dave = { name: 'David Jones', age: 32};
    d.push(5); 
    dave.job = "salesman";
    console.log(d);  // [1, 2, 3, 4, 5]
    console.log(dave);  // { age: 32, job: "salesman", name: 'David Jones'}
}

Problem with block scoping functions函数块级作用域化带来的问题

函数的声明也可以限制在块级作用域中。

{
    bar(); // works
    function bar() { /* do something */ }
}
bar();  // doesn't work

但是当你在一个if语句中声明一个函数的时候问题来了。

想一下这种情况:

if ( something) {
    function baz() { console.log('I passed') }
} else {
    function baz() { console.log('I didn\'t pass') } 
} 
baz();

ES6之前,这两个函数声明都被变量提升,而且结果一定是I didn't pass 不论条件中的something是什么。但现在我们会得到输出ReferenceError, 因为 baz一直被限定在块级作用域内。

PS:该例子个人在chrome上实测,函数声明变量还是被提升了,依然会根据something来选择console内容。

个人评价:块级作用域语法也是经常会用到的语法,关于var,let,和const的具体使用场景我是这么做的。if你声明的变量需要在多个块级作用域内使用并且需要变量提升,那么就选择var;else if你仅仅只希望声明的变量在该块级作用域起作用,不会影响到外面其他作用域就选择let;else 你声明的是一个常量,且不希望它被修改,那么就选择const(注意:如果const的是一个数组或对象时,它还是可以被修改的)。

 

3,扩展运算符

ES6介绍了...操作符,这个操作符指的就是‘扩展运算符‘。他的主要用途有两个:1. 将一个数组或者对象放到一个新的数组或者对象中 2. 将数组中的多个参数合并在一起

第一个用途可能是你将会使用的最多的。所以我们先来看他。

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

如果我们想把一个数组内的一组参数传递给函数,这个时候扩展运算符就十分的有用了。

function foo(a, b, c) { 
console.log(`a=${a}, b=${b}, c=${c}`)
} 
let data = [5, 15, 2];
foo( ...data); // a=5, b=15, c=2

一个对象也可以扩展的,它会把每个键值对写入新的对象中。( 对象扩展已经在提议的第四阶段,而且将会在es2018中正式出现 。但这种特性目前只被chrome60及以后的版本,Firefox55及以后,node 6.4.0及以后的版本所支持)【译者注:在2ality博客中的es2018一文中得知,在刚刚结束的TC39会议中,ECMA2018的特性被敲定了。】

let car = { type: 'vehicle ', wheels: 4};
let fordGt = { make: 'Ford', ...car, model: 'GT'};
console.log(fordGt); // {make: 'Ford', model: 'GT', type: 'vehicle', wheels: 4}

扩展运算符的另一个特点是,他可以生成一个新的数组或者对象. 下面的这个例子,就是b就是新建的数组,但c只是引用同一个数组。

let a = [1, 2, 3];
let b = [ ...a ];
let c = a;
b.push(4);
console.log(a);  // [1, 2, 3]
console.log(b);  // [1, 2, 3, 4] 不同的数组
c.push(5);
console.log(a);  // [1, 2, 3, 5] 
console.log(c);  // [1, 2, 3, 5] 同一个数组

第二个用法是把变量聚集到一个数组里面。当你不知道一个函数到底有多少的传参的时候会这个方法会变得非常的有用。

function foo(...args) {
    console.log(args); 
} 
foo( 'car', 54, 'tree');  //  [ 'car', 54, 'tree' ] 

个人评价:拓展运算符,目前我在实际项目中几乎没使用过,看别人的项目里也没见使用过。可能自己是自己圈子的太局限了。但仔细想想,感觉使用场景还是很多的。如上文所说使用最多的可能就是数组的扩展(目前对象的拓展还没发布,浏览器的支持可能也不太友好,暂时不考虑)。

关于第一个用法:我简单实践了下,并用了数组中最常用的push,见下图。通过对比,发现当我们需要把数组a的所有项加入到b中时,通过...a可以完美实现。直接push(a),得到的是将a真个数组作为整体添加到b中。

ES6小技巧在项目实践中的实用性分析-射手猫的个人博客

关于第二个用法:我也简单实践了下,感觉用处真的不怎么大。见下图。首先,如果你使用该方法时,那么在写function时,传参的形式必须写成...a形式,调用的时候也写成foo(...a)。一是这样写比较麻烦,二是更重要的是这样写就把传参限定死了,必须是数组。这样就会在以后项目中埋下bug隐患。

说它用处不怎么大,是因为我下图中直接将数组a作为传参使用时foo(a),得到的是同样的效果,并且更加简单,且不会将传参类型限定死。

ES6小技巧在项目实践中的实用性分析-射手猫的个人博客

4,参数默认值

函数现在可以使用默认的参数值来定义了。不传参或者未定义值都被初始化为默认值。但是需要注意的是,null和false都会被强转为0.

function foo( a = 5, b = 10) {
    console.log( a + b);
} 
foo();  // 15
foo( 7, 12 );  // 19
foo( undefined, 8 ); // 13
foo( 8 ); // 18
foo( null ); // 10 as null is coerced to 0

默认值的类型可以不仅仅是值类型---还可以是表达式或者函数。

function foo( a ) { return a * 4; }
function bar( x = 2, y = x + 4, z = foo(x)) {
    console.log([ x, y, z ]);
}
bar();  // [ 2, 6, 8 ]
bar( 1, 2, 3 ); //[ 1, 2, 3 ] 
bar( 10, undefined, 3 );  // [ 10, 14, 3 ]

个人评价:目前这个功能几乎不怎么使用。在实际编写时,一般都会将参数默认值放在函数内部编写的,通常也都是在排除一些自己不想要的参数,如null,undefined,NaN等。这种写法在某种特定情况下的确减轻了代码量,但它最大的缺陷就是将null和false都会被强转为0。这个时候你就无法判断传入的参数到底是null还是0。并且0也是经常会用到的判断标准,并不意味着0和null在某种程度上就是相同的。例如我的项目里,判断返回的数据是否正常就是通过data.code===0。所以我的建议是慎用,除非是某些特定情况,完全不需要区别null,false和0的区别。

 

5,解构

解构是拆开等号左边的数组或者对象的过程。这个数组或者对象可以来自一个变量,一个函数,或者一个等式

let [ a, b, c ] = [ 6, 2, 9];
console.log(`a=${a}, b=${b}, c=${c}`); //a=6, b=2, c=9

function foo() { return ['car', 'dog', 6 ]; } 
let [ x, y, z ] = foo();
console.log(`x=${x}, y=${y}, z=${z}`);  // x=car, y=dog, z=6

对象类型的结构,可以在花括号内列出对象的键来提取键值对。

function bar() { return {a: 1, b: 2, c: 3}; }
let { a, c } = bar();
console.log(a); // 1
console.log(c); // 3
console.log(b); // undefined

有时,你可能想提取出值然后费赔给新的变量,这个可以通过在等号左侧使用一个“key:variable”(键:变量名)来完成。

function baz() { 
    return {
        x: 'car',
        y: 'London',
        z: { name: 'John', age: 21}
    }; 
}
let { x: vehicle, y: city, z: { name: driver } } = baz();
console.log(
    `I'm going to ${city} with ${driver} in their ${vehicle}.`
); // I'm going to London with John in their car. 

此外,对象的结构允许给多个变量赋值。

let { x: first, x: second } = { x: 4 };
console.log( first, second ); // 4, 4

个人评价:这里我还是简单总结写解构的用法,可能部分同学光看上面的解释还有点懵懂。

解构最大的特点就是使用类似数组或字面量对象的语法将数组或对象的属性赋给各种变量。

简单来说,就是你等号左边只能写成数组或者字面量对象的形式,不能是一个数组或对象变量。等号右边就没太多限制了,可以是变量或者数组,字面量对象的形式都可以。

写法大概就是

声明 数组/字面量对象 =对应的数组/字面量对象/变量

a,首先是数组解构:

写法:let [a,b,c]=[1,2,3] //等号右边也可以写成array,其中array=[1,2,3]

这样得到结果 a=1,b=2,c=3

值得注意的是,数组解构会有一个对应的原则,左边数组第一个对应右边数组第一个以此类推。

如果你只想取右边数组中的一个或某几个,则需在对应位留空。例如:

let [,b,]=[1,2,3]

这样就只会得到b=2

当然也可以支持不定参数模式

let [a,..b]=[1,2,3]

这样就只会得到a=1 b=[2,3]

b,对象的解构:

写法:let {name:a,age:b,sex:c} = {name:'shotCat',age:18,sex:'man'}

这样就会得到a=shotCat ,b=18,c=man

对象解构也有一个对应原则,但不同数组的那种从前到后的‘一一对应’。它是通过等号右边对象所含的属性名进行对应的。也就是说你左边对象的属性名必须是右边对象存在的属性名。

如果左边的属性名和变量名一致的话,可以用省略写法:

let {name,age,sex} = {name:'shotCat',age:18,sex:'man'}

得到的是变量name=shotCat,age=18,sex=man

注意:如果你没有写声明,或者这些变量之前已经声明了。这样直接去掉声明会导致报错。因为没有了声明,js引擎会直接将左侧的{}解析为一个块语句。解决办法就是将等式加上小括号。例如:({name,age,sex} = {name:'shotCat',age:18,sex:'man'})

 

简单介绍完解构,再谈谈自己对解构在实际开发应用的看法。!再次强调此观点只代表个人看法。

首先解构这种写法第一眼,看上去的确减轻了代码量,也更简洁了,不需要一个一个变量单独进行声明赋值了。如果有大量的变量需要进行这样的赋值(仅仅一两个赋值就没必要了),的确可以减少部分代码量,看起来也比较优雅。但我觉得会有两个弊端:一是代码的维护修改阅读可能不仅仅是你一个人,如果有新手,或者对解构不太了解的程序员维护你的代码时,会对其阅读造成一定的障碍,有时你甚至需要注释说明一下。二是,这样写虽然简单,但阅读起来真的有键值对那种一一对应,清晰明了的感觉吗。我想大部分人阅读到解构的时候,在脑海里还是会自动把它转变成每个变量单独赋值的形式。说到底解构也就是一个语法糖。它简化了我们的写法,但在 实际开发中,更看重的还是条理清晰易维护。即使是减少代码量也不差那么点代码。如果是个人项目,并对这种用法很熟的,当然怎么简单优雅怎么写。

以上也只是我自己的个人看法,如果你有其他想法和见解也欢迎提出,勿喷谢谢。

 

 

6,对象字面量和属性的简洁表达法

当你从许多参数创建对象字面量的时候,ES6允许你在键与变量名字相同的情况下省略该键。

let a = 4, b = 7;
let c = { a: a, b: b };
let concise = { a, b };
console.log(c, concise) // {a: 4, b: 7}, {a: 4, b: 7}

这个还可以与解构一起用来使你的代码更干净整洁。

function foo() {
    return {
        name: 'Anna', 
        age: 56,
       job: { company: 'Tesco', title: 'Manager' }
    };
} 
// pre ES6
let a = foo(), name = a.name, age = a.age, company = a.job.company;
// ES6 destructuring and concise parameters 
let { name, age, job: {company}} = foo();

简洁表示法还可以用于解构对象并把它传入函数。方法1和2是你在es6之前要怎么做, 方法三是使用解构和简洁表达法。

let person = {
    name: 'Anna', 
    age: 56,
    job: { company: 'Tesco', title: 'Manager' }
};
// method 1
function old1( person) {
    var yearOfBirth = 2018 - person.age;
    console.log( `${ person.name } works at ${ person.job.company } and was born in ${ yearOfBirth }.`);
}
// method 2
function old1( person) {
    var age = person.age,
        yearOfBirth = 2018 - age, 
        name = person.name,
        company = person.job.company;
    console.log( `${ name } works at ${ company } and was born in ${ yearOfBirth }.`);
} 
// method 3
function es6({ age, name, job: {company}}) {
    var yearOfBirth = 2018 - age;
    console.log( `${ name } works at ${ company } and was born in ${ yearOfBirth }.`);
} 

通过使用ES6,我们能提取出agename,和 company,而不需要任何其他的变量声明。

个人评价:这种省略的写法我上面解构中,提到过。这种写法我的看法和解构一样,没必要为这么点代码量而省略。还是那句话如果是个人项目,并对这种用法很熟的,当然怎么简单优雅怎么写。

 

 

7,动态属性名称

ES6添加了使用动态分配的键创建或添加属性的功能。

let  city= 'sheffield_';
let a = {
    [ city + 'population' ]: 350000
};
a[ city + 'county' ] = 'South Yorkshire';
console.log(a); // {sheffield_population: 350000, sheffield_county: 'South Yorkshire' }

个人评价:这个用得不是很多,在我接触的项目中大部分属性名都是之前已经约定好了。用的话可能也就是遍历的时候会比较多,需要拼接出大量前缀相同(也有可能是其他部分相同)的属性名。但一般情况下是拼接属性值,很少会去拼接属性名。所以意见就是,如果有这个需求的话,是推荐使用的,尤其是需要遍历拼接大量属性名时。

 

8,箭头函数

箭头函数有两个比较重要的特点: 他们的结构以及他们的this 指向

他们比传统的函数有更简单的结构因为他们不需要关键字function 而且他们可以自动返回在箭头后面的一部分,无论箭头后面的是什么。

var foo = function( a, b ) {
    return a * b;
} 
let bar = ( a, b ) => a * b;

如果函数有多于一个的计算式,可以使用花括号来包起来,然后函数返回块作用域返回的任何内容。

箭头函数一个最重要的用途之一就是应用在数组的相关函数中,像.map.forEach,.sort等等。

let arr = [ 5, 6, 7, 8, 'a' ];
let b = arr.map( item => item + 3 );
console.log(b); // [ 8, 9, 10, 11, 'a3' ]

在拥有一个更短的表达方式的同时,箭头函数还修复了有关于this绑定行为经常出现的问题。ES6之前解决这个问题通常是使用一个self变量来存储这个指向。

var clickController = {
    doSomething: function (..) {
        var self = this;
        btn.addEventListener(
            'click', 
            function() { self.doSomething(..) }, 
            False
       );
   } 
};

这个this的赋值是一定要做的,因为this的绑定是动态的。这就意味着this在eventlistener内部和在doSomething内部指的并不是同一个东西。

在箭头函数内部,this的绑定是语义上的就是指当前的,而不是动态的。这也是箭头函数的主要设计特点。

虽然这种词法上的this很棒,但是有些时候,他却不是我们想要的那样。

let a = {
    oneThing: ( a ) => {
         let b = a * 2;
         this.otherThing(b);
    }, 
    otherThing: ( b ) => {....} 
};
a.oneThing(6);

当我们使用a.oneThing(6), 这个this.otherThing(6) 会抛出引用失败的错误,因为this没有指向对象a,而是指向了环境作用域。如果你正在使用ES6的代码使用ES6的语法,这个是你需要注意的事情。

个人评价:这个是用得非常多的,每个前端er都应该熟练掌握的。如果你不懂,你就看不懂别人的代码。

在这里,我想延伸多问一句,箭头函数可以用作构造函数吗?

如果你真正理解了它的this指向,答案就很显然是不能!这里我推荐一篇文章 什么时候不能使用箭头函数?

 

 

9,for...of loops (for...of循环)

ES6新添加了一种方式来迭代数组中的每个值,这个方式是与已经存在的for...in的通过索引的循环方式不同。

let a = ['a', 'b', 'c', 'd' ];
// ES6 
for ( var val of a ) {
    console.log( val );
} // "a" "b" "c" "d"
// pre-ES6 
for ( var idx in a ) {
    console.log( idx );
}  // 0 1 2 3

使用新的for ... of循环,在每个循环内部保存了一个let val = a[idx]

数组,字符串,generator以及从collection 在标准JavaScript中都是可迭代的。普通的对象无法正常的使用for...of来迭代,除非你自己定义一个迭代器。

个人评价:我之前大部分迭代数组都是使用forEach或者是下面这种写法。

for (var i = 0; i < array.length; i++) {
var obj = array[i];
}但是forEach当中是不能使用break和return。但使用for of就没有这个限制,并且更简洁。所以下次循环数组时,推荐使用for of!

10,Number Literals 数字字面量

ES5代码很好处理了十进制和十六进制的数字格式,但并未指定八进制的格式。实际上,八进制在严格模式中是被禁止使用的。

ES6 添加了一个全新的格式,在最开始的0后面添加一个o来声明一个八进制的数。与此同时,在es6中还添加了二进制格式。

Number( 29 )  // 29
Number( 035 ) // 35 in old octal form. 
Number( 0o35 ) // 29 in new octal form 
Number( 0x1d ) // 29 in hexadecimal 
Number( 0b11101 ) // 29 in binary form

个人评价:自己所接触的项目中,目前还未涉及到必须八进制。所以如果有这个需求的同学可以尝试这种新的方法。