let和const
**ES5有两种定义变量的方法:var和function。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; // 报错
}
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命令,这个区块中let和const声明的变量,从一开始就形成了封闭作用域(块级作用域),凡是在声明之前使用
这些变量,都会报错。即为暂时性死区
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 声明一个只读的常量,一旦声明,值就不能改变
- const变量声明时就必须立即初始化,且不能重新赋值
- const变量和let变量一样,无变量提升,有块级作用域,不能重复声明
- 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都能取到值
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
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:2,
bar: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"
模版字符串也可以嵌套。
函数扩展
- 函数参数的默认值
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. 参数变量是默认声明的,所以不能用let或const再次声明
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的值就是undefined
例2:
let 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方法
- 扩展运算符的应用
. 合并数组//ES5 [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.
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。
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 }
如果扩展运算符的参数是null或undefined,这两个值会被忽略,不会报错。
let emptyObject = { ...null, ...undefined }; // 不报错
模块
ES6模块的设计思想是尽量的静态化,是的编译时就能确定模块的依赖关系,以及输入和输出的变量。即ES6可以在编译时就完成模块
加载。模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
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,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
同样的,function和class的输出,也必须遵守这样的写法。
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
5. export和import不可放在块级作用域内
- 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();
temp
字符串扩展
- JS内部字符是以utf-16格式存储,每个字符固定2个字节。对于那些需要4个字节存储的(Unicode 码点大于0xFFFF的字符),JS会认为它们是两个字符。
- 字符串可以使用for...of遍历
- ES5只提供了indexof来查找字串,ES6又提供了3种方法
- includes() 返回布尔值,表示是否找到字串
- startsWith() 返回布尔值,表示字串是否在原字符串的开头
- 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"
正则扩展
- 在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]