ES6语法

159 阅读23分钟

定义变量的方法

let和const

​ ES5有两种定义变量的方法:varfunction

​ ES6一共有6中声明变量的方法。

  • var
  • function
  • let
  • const
  • import
  • class

let关键字

基本语法

1. let声明变量只在其所在的代码块内有效
{
   let a = 10;
   var b = 1;
}

a // 报错,ReferenceError: a is not defined
b // 1 

2. for循环中用let定义的变量只在循环体内有效,在循环体外就无法使用,和var不同。并且for()中设置循环变量的是一个父作用域,而
循环体内是另外一个单独的作用域。
for(let i = 0; i < 3;++i){
   let i = 'abc';
   console.log(i)
}
输出:
abc abc abc   //可见是两个单独的作用域

3. 不允许重复声明变量  let不允许在相同作用域内,重复声明一个变量
function(){
   let a = 10;
   var a = 1;  //报错 
}

function(){
   let a = 10;
   let a = 88;  //报错 
}

//因此不能在函数内部用let重新声明参数
function func(arg) {
   let arg; // 报错
}
}

不存在变量提升

var定义变量会产生变量提升的现象,即变量可以在声明之前使用,值为undefined。而let定义的变量,一定要在声明之后使用,否则就会报错。
console.log(foo); //输出undefined
var foo = 2;

console.log(bar);  //let变量声明之前使用,会报错ReferenceError
let bar = 2;

暂时性死区(TDZ)

如果区块中存在let和const命令,这个区块中letconst声明的变量,从一开始就形成了封闭作用域(块级作用域),凡是在声明之前使用。

这些变量,都会报错。即为暂时性死区。

1. 
var tmp = 123;
if(true){
    tmp = 'abc'; //报错,ReferenceError
    let tmp;
}
// 上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明之前使用
对tmp赋值就会报错。

有了暂时性死区的存在,也即意味着typeof会报错
typeof x  //x 被let定义,这样使用typeof就会报错,ReferenceError。如果没有用let声明,typeof会显示undefined
let x 

2. 有一些死区比较隐蔽
function bar(x = y, y = 2){
    return [x,y]
}
bar()  //会报错,因为y还未声明

function bar(x = 2, y = x){
    return [x,y]
}
bar()  //此时不会报错,因为x已声明

3. 下面这样也会报错
var x = x ;  //不报错

let x = x ;  //报错, x is not defined。因为x在未声明之前使用

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

let实际上为JS新增了块级作用域

1. function f1(){
    let n = 5;
    if(true){
        let n = 10;
    }
    console.log(n)  //5 
    //如果两次都用var,会输出10
}

2. ES6允许块级作用域的任意嵌套
{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

内层作用域可以定义外层作用域的同名变量。
{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

const关键字

  1. const变量声明时就必须立即初始化,且不能重新赋值
  2. const变量和let变量一样,无变量提升,有块级作用域,不能重复声明
  3. const定义对象或数组时,只是变量的指向的地址不能改变,而对象可以添加修改属性
const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only


const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错 

如果想冻结对象,可以使用Object.freeze
const foo = Object.freeze({});  //foo指向一个被冻结的对象,所以不能添加属性
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123; 

解构赋值

从数组和对象中提取值,对变量进行赋值。

数组解构赋值

1. 
//这种写法属于模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值
let [a,b,c] = [1,2,3]

//一些例子
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

2. 如果解构不成功,变量的值就等于undefined
let [foo] = []  //foo的值为undefined
let [bar,foo] = [1]  //foo的值也为undefined

3. 不完全解构,即等号左边的模式 只匹配一部分的等号右边的数组
let [x,y] = [1,2,3] // x = 1 ,y = 2 

let [a,[b],d] = [1,[2,3],4]  //a=1,b=2,d=4

4. 如果等号右边的不是可遍历的结构,就会报错
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
//上面几个表达式,前5个等号右边转换为对象后不具备Iterator接口,最后一个本身不具备Iterator接口
只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值,比如Set
let[x,y,z] = new Set(['a','b','c']); //x = 'a' 

默认值

解构赋值允许指定默认值。

1. 
let [foo=true] =[]  //foo=true 
let [x,y='b'] = ['a']  //x='a',y='b'
let [x,y='b'] = ['a',undefined] //x='a',y='b'

2. ES6使用严格相等运算符(===)判断一个位置是否有值,所以一个位置上不严格等于undefined,默认值不会生效
let [x=1] = [undefined]  //默认值生效,x=1 
let [x=1] = [null]  //默认值不生效,因为null不严格等于undefined,x=null 

3. 默认值可以引用解构赋值的其他变量,但是该变量必须已经申明
let [x=1,y=x] = [];  //x=1,y=1 
let [x=1,y=x] =[];   //x=1,y=2
let [x=1,y=x] =[1,2] //x=1,y=2
let [x=y,y=1] = [];  //报错,因为x=y时,y还没有声明

对象的解构赋值

1. 对象的解构与数组的不同是,数组的元素必须是按次序排列的,变量的取值由它的位置决定;而对象的属性与次序无关,变量必须与属性
同名,才能取得正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

2. 如果变量名与属性名不一致,必须写成下面形式
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined  

这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》)
let { foo,bar } = { foo: "aaa", bar: "bbb" }等同于下面
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
====================================================================
//foo: foo  冒号左边是要与等号右边匹配的模式(属性),冒号右边才是要被赋值的变量 ,如果匹配不上,变量的值就为undefined
=====================================================================

也就是说,对象解构的内部机制是先找到同名属性,然后在赋值给对应的变量的值。真正被赋值的是后者,而不是前者
所以上面第二个例子,baz有值,foo无值。foo是匹配的模式,baz才是变量,真正被赋值的是baz。

3. 对象解构也可以嵌套
let obj = {
    p:[
     'hello',
      {y:'world'}
    ]
};

let {p:[x,{y}]} = obj;  //x='hello',y='world'
此时p是模式,不是变量,因此不会被赋值

若想P也作为变量赋值,可以写成:
let obj = {
    p:[
     'hello',
      {y:'world'}
    ]
};

let {p,p:[x,{y}]} = obj; //x='hello',y='world',p = ["Hello", {y: "World"}]
再比如:
var node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

var { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

上面代码只有三次解构赋值,分别是对loc,start,line三个属性的解构赋值。最后一次对line属性的解构赋值,只有line是变量,
loc和start都是模式,不是变量

还可以这样嵌套赋值
let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]

4. 对象的解构也可以指定默认值,默认值生效的条件也是对象的属性值严格等于undefined
var {x=3} ={}  //x=3;
var {x,y=5} ={x:1} //x=1,y=5
var {x:y=3} ={}  //y=3;
var {x:y=3} = {x:5} //y=5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

var {x=3} = {x:undefined}  //x=3;
var {x=3} = {x:null}; //x=null 

如果解构失败,变量的值等于undefined
let {foo} = {bar: 'baz'};
foo // undefined

5. 如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

// 报错
let {foo: {bar}} = {baz: 'baz'};
上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。

6. 如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

7. 对象的解构赋值,可以很方便的将现有对象的方法,赋值到某个变量
let { log, sin, cos } = Math;
上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。

8. 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”,参见对象的扩展。

字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

1. function add([x,y]){
    return x + y;
}
add([1,2]) //3 
上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

function g({name: x}) {
  console.log(x); //5 
}
g({name: 5})

再比如:
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

再如:
let cat ={
        run:function(){
        console.log('Running');
        }
};

 function test({run}){
        run();
 }

test(cat); //传入含有run属性的对象即可

2. 函数参数的解构也可以使用默认值。

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

上面代码中,函数move的参数是一个对象,即等号右边的{},通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

下面是和上面不同的情况:
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
move(undefined);//[0,0]
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

undefined就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

解构赋值的用途

1. 交换变量值
let x  =  1;
let y = 2;
[x,y] = [y,x]


2. 从函数返回多个值,不用将它们放进一个数组或者对象了
//返回一个数组
function example(){
    return [1,2,3];
}

let [a,b,c] = example();

//返回一个对象
function example(){
    return {
        foo:2bar:2
    };
}

let {foo,bar} = example();

3. 函数参数定义
解构赋值可以方便地将一组参数与变量名对应起来。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

4. 提取JSON数据
解构赋值对提取JSON对象中的数据,尤其有用。

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

5. 函数参数默认值
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。

6. 遍历Map结构

任何部署了Iterator接口的对象,都可以用forof循环遍历。Map结构原生支持Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。

var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

7.输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

字符串扩展

模板字符串

​ 模板字符串是增强版的字符串,用反引号(`)标识,可以当做普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
var greeting = `\`Yo\` World!`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。可以使用trim()去除。

模板字符串中嵌入变量,需要将变量名写在${}之中,如果模板字符串中的变量没有声明,将报错变量未声明

花括号内部可以放入任意的JS表达式,可以进行计算,以及可以引用对象
var x = 1,y=2

`${x} + ${y} = ${x+y}`  //1+2=3

`${x} + ${y * 2} = ${x + y * 2}`  // "1 + 4 = 5"

var obj ={x:1,y:2}
`${obj.x + obj.y}`  //3 

模板字符串还能调用函数
function fn(){
    return 'Hello'
}
`foo ${fn()} bar`  //foo Hello World bar 

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。

如果大括号中的变量未申明,则会报错。

由于模板字符串的大括号内部,就是执行JavaScript代码,因此如果大括号内部是一个字符串,将会原样输出。

`Hello ${'World'}`
// "Hello World"

模版字符串也可以嵌套。

方法扩展

  • JS内部字符是以utf-16格式存储,每个字符固定2个字节。对于那些需要4个字节存储的(Unicode 码点大于0xFFFF的字符),JS会认为它们是两个字符。
  • 字符串可以使用for...of遍历
  • ES5只提供了indexof来查找字串,ES6又提供了3种方法
  1. includes() 返回布尔值,表示是否找到字串
  2. startsWith() 返回布尔值,表示字串是否在原字符串的开头
  3. endsWith() 返回布尔值,表示字串是否在原字符串的结尾
1. 
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

2.三个方法都支持第二个参数,表示搜索开始的位置
let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

其中endWith的第二个参数与其他两个不同,它表示前n个字符,其他两个表示从第n个位置直到结束。

repeat()方法

​ 返回一个新字符串,表示原字符串重复n次

1.
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

2. 如果参数是小数,会被取整
'aa'.repeat(2.9) // aaaaaa

如果参数是负数或者Infinity,会报错
'aa'.repeat(Infinity)   //  RangeError: Invalid count value
'aa'.repeat(-1)  // RangeError: Invalid count value

如果参数是0~-1 之间的小数,会被先取整为-0,等价与0
'aa'.repeat(-0.9)  // ""

参数NaN等同于0 
'aa'.repeat(NaN);   // ""

如果参数是字符串,则会先转换为数字
'aa'.repeat('abc');    // ""
'aa'.repeat('2');   //aaaa
'aa'.repeat(undefined)   // ""
'aa'.repeat(null)   // "" 

padStart()方法用于头部补全长度

padEnd()用于尾部补全长度

1. padStart()和padEnd()一共接受两个参数,第一个参数表示指定字符串的最小长度,第二个参数是用来补全的字符串。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

2. 如果原字符串的长度,等于或大于 第一个参数,则返回原字符串
'xxxxx'.padStart(5,'aa')  //  'xxxxx'
'xxxxx'.padEnd(5,'aa')   //  'xxxxx'

3. 如果补全的字符串和原字符串的长度加起来 超过了第一个参数,则会截取超过长度的补全字符串
'abc'.padStart(10, '0123456789')    // '0123456abc'

4. 如果省略了第二个参数,则默认使用空格补全长度。
'xxx'.padStart('7')   // "    xxx"

5. 第一个参数如果是字符串,则会转换为数字 
'xxx'.padStart('6a','abc') //  "xxx"
'xxx'.padStart('yyyy','abc') //  "xxx"
'xxx'.padStart(null,'abc')   //  "xxx"
'xxx'.padStart(undefined,'abc') //  "xxx"   

6. 常用操作
  1)为数字补全指定位数
        '1'.padStart(10, '0') // "0000000001"
        '12'.padStart(10, '0') // "0000000012"
        '123456'.padStart(10, '0') // "0000123456"
  2)设置字符串格式
        '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
        '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

函数扩展

函数参数的默认值

1. ES5中为函数参数设置默认值的方法:
function log(x,y){
    y = y || 'world';
    console.log(x,y)
}

log('hello')  //hello world
log('hello','china')  //hello china 
log('hello','')  //hello world 
上面写法的缺点是,y赋值了,但是对应的bool值为false,则y还是取world。所以还需要判断参数y时否为赋值,如果没有再等于默认值
if(typeof y === 'undefined'){
    y = 'world'
}
而ES6中可以设置默认值
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

2. 参数变量是默认声明的,所以不能用letconst再次声明

3. 使用参数默认值时,函数名不能有同名参数
// 不报错
function foo(x, x, y) {
  // ...
}

// 报错
function foo(x, x, y = 1) {
  // ...
}

4. 另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100

与解构赋值默认值结合使用

1. function foo({x,y=5}){
    console.log(x,y)
}
foo({}) // undefined 5 
foo({x:1}) //1 5 
foo({x:1,y:2})  //1,2
foo() //TypeError: Cannot read property 'x' of undefined 

上面只是使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没有提供参数,变量x和y就不会生成,从而报错。通过设置函数参数的默认值,就可以避免这种情况

function foo({x,y=5} = {}){
    consoel.log(x,y)
}
foo() //undefined 5 
上面的定义,如果没有给函数传参数,函数foo的默认参数就是一个空对象({})

2. 解构默认值与参数默认值结合
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 报错

上面代码中,函数fetch第二个参数是一个对象,如果传入一个对象就可以为它的三个属性设置默认值。而省略第二个参数就会报错。
结合函数参数默认值,就可以省略第二个参数了。就设置了双重默认值。
function fetch(url,{method = 'GET'} = {}){
    console.log(method)
}
fetch('http://example.com')
// "GET"

上面代码中,参数fetch没有第二个参数时,函数参数的默认值就是一个空对象,然后才是解构赋值的默认值生效。变量method才会取到默认值GET。

于是就有了下面两种写法:
//写法1
function m1({x=0,y=0} = {}){
    return [x,y]
}
//写法2 
function m2({x,y} = {x:0,y:0}){
    return [x,y]
}

上面的代码,写法1设置了函数参数的默认值是一个空对象,但是设置了对象解构赋值的默认值
写法2函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

参数默认值的位置

​ 如果函数参数的默认值不是最后一个,则这个位置的参数无法省略。

//例一
function f(x=1,y){
    return [x,y]
}
f() //[1,undefined]
f(2) //[2,undefined]
f(,1) //报错
f(undefined,1) //[1,1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
f(1,undefined) //[1,5,undefined]  

有默认值的参数都不是最后一个参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

函数的length属性

​ 函数参数设置了默认值以后,函数的length属性的含义是该函数预期传入的参数个数。

1. 参数默认值是最后面的,则length返回的是没有指定默认值得参数个数,使用参数总数减去默认值的个数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a,b=1,c=2) {}).length //1 

length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。
(function(...args){}).length //0

2. 参数默认值不是最后面的,则默认值后面的参数就不再计算
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
(function (a,b=1,c,d=4) {}).length // 1 
(function (a,b=1,c=2,d) {}).length // 1 

作用域

​ 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束时,这个作用域就会消失。而不设置参数默认值时,就没有此作用域。

1:
var x = 1;
function f(x,y=x){
    consoel.log(y);
}
f(2)  //2 
f()  //undefined 

//参数y的默认值等于变量x,调用函数f时,参数形成一个单独的作用域。在这个作用域里,y的默认值x指向第一个参数x,而不是全局变量x,
所以输出2。不传值时,y的值就是undefined2let x = 1;
function f(y=x){
    let x = 2;
    console.log(y);
}
f() //1 

//未完待续

rest参数

​ rest参数形式为(...参数名)用于获取函数的多余参数,这样就不要argument对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

1. function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

2. 
// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。

3. 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}
函数的length属性,不包括 rest 参数。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

箭头函数

​ 箭头函数等同于匿名函数

var f = v=>v;

等同于
var f = function(v){
    return v;
}

​ 如果箭头函数不需要参数或这需要多个参数,就使用一个圆括号代表参数部分

var f = ()=>5;
等同于:
var f = function(){
    return 5;
}

//多个参数
var sum = (num1,num2)=>num1+num2 
等同于:
var sum = function(num1,num2){
    return num1 + num2 
}

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
let fn = () => void doesNotReturn();

箭头函数可以与变量解构结合使用
const full = ({first,last})=>first + '' + last 
等同于:
function full(person){
    return person.first + '' + person.last;
 }

箭头函数使得表达更加简洁。
const isEven = n => n % 2 == 0;
const square = n => n * n;
上面代码只用了两行,就定义了两个简单的工具函数

​ 箭头函数简化回调函数

1.1 
//正常写法:
[1,2,3].map(function(x){
    return x*x;
})

//箭头函数写法
[1,2,3].map(x=>x*x)

2.2 
// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

3. rest参数与箭头函数结合的例子
const numbers = (...nums) => nums 
numbers(1,2,3,4,5) //[1,2,3,4,5]

const headTail = (head,...tail)=>[head,tail]
headTail(1,2,3,4,5)  //[1,[2,3,4,5]]

​ 箭头函数注意事项

1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

​ 箭头函数的this 绑定定义时所在的作用域(父级上下文),而不是指向运行时所在的作用域

//ES6 
function foo(){
    setTimeout(()=>
    {
        console.log('id:',this.id)
    },1000)
}
// 上面代码的this指向转换为ES5为:
function foo(){
    var _this = this;
    setTimeout(function(){
        console.log('id:',_this.id);
    },1000);
}
上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

数组扩展

扩展运算符

​ 扩展运算符好比rest参数的逆运算,将一个数组转换为用逗号分隔的参数序列

console.log(...[1,2,3])  //1 2 3 
console.log(1,...[2,3,4],5) //1 2 3 4 5
[...document.querySelectorAll('div')]  // [<div>, <div>, <div>]

如果扩展运算符后面是一个空数组,则不产生任何效果
[...[],1]  //[1] 

​ 主要用于函数调用

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

var numbers = [4, 38];
add(...numbers) // 42

扩展运算符将一个数组,变为参数序列。

一个例子是通过push函数,将一个数组添加到另一个数组的尾部。

// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。
有了扩展运算符,就可以直接将数组传入push方法

扩展运算符的作用

合并数组

[1,2].concat(more)
    //ES6 
    [1,2,...more]

    var arr1 = ['a','b'];
    var arr2 = ['c'];
    var arr3 = ['d','e'];
    //ES5合并:
    arr1.concat(arr2,arr3);  //['a','b','c','d','e']

    //ES6合并
    [...arr1,...arr2,...arr3] 

对象扩展

属性的简洁表示

ES6允许直接写入变量和函数,作为对象的属性和方法
1. 
var foo = 'bar';
var baz = {foo}
baz //{foo:'bar'}

等同于:
var baz = {foo:foo}

ES6允许在对象之中,直接写变量。这是,属性名为变量名,属性值为变量的值。
function f(x, y) {
  return {x, y};
}

// 等同于

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

2. 除了属性简写,方法也可以简写
var o = {
    method(){
        return 'hello';
    }
}
等同于:
var o = {
    method:function(){
        return 'hello';
    }
}

再比如:
var birth = '2000/01/01';

var Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

3. 用于函数返回值
function getPoint(){
    var x =1;
    var y =10;
    return {x,y}
}

getPoint() //{x:1,y:10}

对象扩展运算符(...)

1、 
使用对象扩展符的解构赋值用于从一个对象取值,将所有的可遍历的,但尚未被读取的属性,分配到指定的对象上面。所有的键和值都会
被拷贝到新对象上面。

let {x,y,...z} = {x:1,y:2,a:3,b:4}
x //1 
y //2
z //{a:3,b:4}
变量z是解构赋值所在的对象,它获取等号右边的所有尚未读取的值(a,b),连同值一起拷贝过来。

2. 
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。
let { x, y, ...z } = null; // 运行时错误
let { x, y, ...z } = undefined; // 运行时错误

3. 
扩展运算符必须是最后一个参数,否则会报错。

let { ...x, y, z } = obj; // 句法错误
let { x, ...y, ...z } = obj; // 句法错误

4. 解构赋值的拷贝是浅拷贝,即如果一个键的值是(数组,对象,函数),那么解构赋值拷贝的是这个值得引用,而不是这个值得拷贝
let obj = {a:{b:1}}
let {...x} = obj
obj.a.b = 2;
x.a.b = 2 
上面代码中,x是解构赋值所在的对象,拷贝了对象obj的a属性。a属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

5. 另外,解构赋值不会拷贝继承自原型对象的属性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
上面代码中,对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性

再比如:
var o = Object.create({x:1,y:2});
o.z = 3 

let {x,...{y,z}} = o;
x //1 
y //undefined 
z //3 

上面代码中,x只是单纯的解构赋值,所以可以读取对象o的属性,变量y和z是双重解构赋值,只能读取对象o自身的属性,所以只有变量z
赋值成功。

6. 解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用x和y参数进行操作
  // 其余参数传给原始函数
  return baseFunction(restConfig);
}
上面代码中,原始函数baseFunction接受a和b作为参数,函数wrapperFunction在baseFunction的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。

扩展运算符

1. 扩展运算符用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
let z = {a:3,b:4}
let n = {...z}
n //{a:3,b:4}

2. 合并两个对象
let ab = {...a,...b}
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
这用来修改现有对象部分的属性就很方便了。

let newVersion = {
  ...previousVersion,
  name: 'New Name' // Override the name property
};
上面newVersion自定义了name属性,其他属性全部复制于previousVersion

3.
 如果扩展运算符后面是一个空对象,则没有任何效果。
{...{}, a: 1}
// { a: 1 }


如果扩展运算符的参数是nullundefined,这两个值会被忽略,不会报错。

let emptyObject = { ...null, ...undefined }; // 不报错

模块

  • ES6模块的设计思想是尽量的静态化,是的编译时就能确定模块的依赖关系,以及输入和输出的变量。即ES6可以在编译时就完成模块加载。

  • 模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

  • export除了输出变量,还可以输出函数和类

export

1. 输出变量:
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
等同于:
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

2. 输出函数:
export function multiply(x, y) {
  return x * y;
};

3. 通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。

4. export命令规定的是对外的接口,必须与模块内部的变量建立一对一关系
// 报错
export 1;

// 报错
var m = 1;
export m;
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出11只是一个值,不是接口。正确的写法是下面这样。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

同样的,functionclass的输出,也必须遵守这样的写法。

// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

5. exportimport不可放在块级作用域内 

import

1. 
// main.js
import {firstName, lastName, year} from './profile';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile';

2. import 的.js可以省略。可以使用相对和绝对路径,如果只是模块名,不带有路径,需配置路径。

3. import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();

import { foo } from 'my_module';
上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

4. import * as o  from xxx  可以加载所有输出值到对象o上

export default

​ 为模块指定默认输出

1. 使用export default 导出时,import导入可以指定任意名字,且不能使用大括号
// export-default.js
export default function () {
  console.log('foo');
}
上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'
上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。
需要注意的是,这时import命令后面,不使用大括号

2. export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。

3. 
因为export default本质是将该命令后面的值,赋给default变量以后再默认,所以可以直接将一个值写在export default之后。

// 正确
export default 42;

// 报错
export 42;
上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为default

4. export default也可以用来输出类。

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

正则扩展

​ 在ES5中,RegExp构造函数有两种情况

1. 参数是字符串,此时第二个参数是表示正则表达式的修饰符
var regex = new RegExp('xyz','i')
等价于
var regex = /xyz/i;

2. 参数是一个正则表达式,这是会返回一个原正则表达式的拷贝
var regex = new RegExp(/xyz/i)
等价于
var regex = /xyz/i;
但是上面这种情况,ES5中不允许使用第二个参数添加修饰符,否则会报错。

ES6改变了这种情况,如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。并且添加的参数修饰符,会覆盖原有的修饰符,只使用指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"

Es5的source属性,会返回正则表达式的正文。
/abc/ig.source     // 'abc'

Es6增加了flags属性,返回正则表达式的修饰符
/abc/ig.flags      // 'ig'

​ 字符串的正则方法

字符串共有四个方法,可以使用正则表达式
match()   replace()   search()   split()  
Es6将这四个方法,在内部全部调用RegExp()的实例方法,从而将所有与正则有关的方法都定义在RegExp上。

1. String.prototype.match 调用 RegExp.prototype[Symbol.match]
2. String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
3. String.prototype.search 调用 RegExp.prototype[Symbol.search]
4. String.prototype.split 调用 RegExp.prototype[Symbol.split]