ES6全套教程

1.let和const命令

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
上面代码中,变量i是var声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的function在运行时,会通过闭包读到这同一个变量i,导致最后输出的是最后一轮的i的值,也就是10。
console.log(a)//[function(){console.log(i)},function(){console.log(i)},function(){console.log(i)}]

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

for(let i=0;i<3;i++)
{
  let i="abc"
  console.log(i)
}
//"abc"
//"abc"
//"abc"
说明内部变量不会影响到外部变量

function bar(x=y,y=2)
{
  return [x,y]
}
bar()//报错,y没有申明

function bar(x=2,y=x)
{
   return [x,y]
}
bar()//[2,2]

2.不允许重复申明
let 不允许在相同作用域内,重复声明同一个变量

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

不能在函数内部重新声明参数

function func(arg)
{
  let arg;//报错
}
function func(arg)
{
   {
     let arg;//不报错
   }
}

3.块级作用域与函数申明
1.允许在块级作用域内声明函数。
2.函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
3.同时,函数声明还会提升到所在的块级作用域的头部。

function f(){console.log('i am outside!')}]
(function(){
    if(false){
        function f(){console.log('i am inside')}
      }
      f()
 }())
 //error:f is not a function
 实际运行的是如下代码:
 //浏览器的es6环境
 function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }
  f();
}());
//浏览器的es5环境//函数会提升到函数的头部
function f(){console.log("i am outside!")}
(function (){
    function f(){console.log("i am inside!")}
    if(false){
    }
    f()
}())
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 函数声明语句
{
  let a = 'secret';
  function f() {
    return a;
  }
}
// 函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

4.const
const声明的常量,也与let一样不可重复声明。

var a=1;
window.a//1

let b=1;
window.b//undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。

变量的解构赋值

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'
注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
let [x=1]=[undefined];//x//1
let [x=1]=[null]//x=null

2.对象的解构赋值

let {bar,foo}={foo:"aaa",bar:"bbb"}
foo//'aaa'
bar//'bbb'
let {baz}={foo:"aaa",bar:"bbb"}
baz//undefined
var {foo:baz}={foo:"aaa",bar:"bbb"}
baz//"aaa"
注:foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

let foo;
let {foo}={foo:1}//SyntaxError:Duplicate declaration "foo"

let baz;
let {bar:baz}={bar:1}//SyntaxError:Duplicate declaration "baz"
上面代码中,解构赋值的变量都会重新声明,所以报错了。不过,因为var命令允许重新声明,所以这个错误只会在使用letconst命令时出现。如果没有第二个let命令,上面的代码就不会报错。
let foo;
({foo}={foo:1});//成功
let baz;
({bar:baz}={bar:1})//成功
上面代码中,let命令下面一行的圆括号是必须的,否则会报错。因为解析器会将起首的大括号,理解成一个代码块,而不是赋值语句。
let obj={
  p:[
    "Hello",
    {y:"world"}
   ]
}
let {p:[x,{y}]}=obj;
x//"Hello"
y//"world"

对象的解构也可以指定默认值。
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
默认值生效的条件是,对象的属性值严格等于undefined
var {x=3}={x:undefined};x//3
var {x=3}={x:null};x//null
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
//报错
let {foo:{bar}}={bar:'baz'}
let _tmp={baz:"baz"}
_tmp.foo.bar//报错

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

const [a,b,c,d,e]="hello"
a//"h"
b//"e"
c//"l"
d//"l"
e//"o"
let {length:len}="hello"
console.log(len)//5

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

let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let {prop:x}=undefined //TypeError
let {prop:y}=null//TypeError

5.函数参数的解构赋值
6.用途
1.交换变量的值

let x=1;
let y=2;
[x,y]=[y,x];
上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。

2.从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

//返回一个数组
function example(){
return [1,2,3]
}
let [a,b,c]=example();
//返回一个对象
function example(){
return {
   foo:1,
   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.遍历Map结构
任何部署了Iterator接口的对象,都可以用for…of循环遍历。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

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

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

字符串的扩展

1.es6为字符串添加了遍历器接口,使得字符串可以被for…of循环遍历。

for(let codePoint of "foo")
{
   console.log(codePoint)
}
输出的结果:“f”,“o”,“o”

2.at()
ES5对字符串对象提供charAt方法,返回字符串给定位置的字符。该方法不能识别码点大于0xFFFF的字符。

'abc'.charAt(0) // "a"
''.charAt(0) // "\uD842"

上面代码中,charAt方法返回的是UTF-16编码的第一个字节,实际上是无法显示的。
目前,有一个提案,提出字符串实例的at方法,可以识别Unicode编号大于0xFFFF的字符,返回正确的字符。

'abc'.at(0) // "a"
''.at(0) // ""

3.includes(),startsWith(),endsWith()
传统上,JavaScript只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。

var s="Hello world"
s.startsWith("Hello")//true
s.endsWith("d")//true
s.includes("o")//true
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

4.repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。

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

参数如果是小数,会被取整

'na'.repeat(2.9) // "nana"

如果repeat的参数是负数或者是Infinity,会报错。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

但是,如果参数是0到-1之间的小数,则等同于0,这是因为会先进行取整运算。0到-1之间的小数,取整以后等于-0,repeat视同为0。

'na'.repeat(-0.9) // ""

参数NaN等同于0。

'na'.repeat(NaN) // ""

如果repeat的参数是字符串,则会先转换成数字。

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

数值的扩展

1.Number.isFinite(),Number.isNaN()
ES6在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。
Number.isFinite()用来检查一个数值是否为有限的(finite)。

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN()用来检查一个值是否为NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true

2.Number.isInteger()
Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

3.Number.EPSILON
ES6在Number对象上面,新增一个极小的常量Number.EPSILON。

Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'

引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
但是如果这个误差能够小于Number.EPSILON,我们就可以认为得到了正确结果。

5.551115123125783e-17 < Number.EPSILON
// true

function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON;
}
withinErrorMargin(0.1 + 0.2, 0.3)
// true
withinErrorMargin(0.2 + 0.2, 0.3)
// false

4.安全整数和Number.isSafeInteger()
JavaScript能够准确表示的整数范围在-253到253之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992

9007199254740992  // 9007199254740992
9007199254740993  // 9007199254740992

Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

上面代码中,超出2的53次方之后,一个数就不精确了。
ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true

通过使用Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
5.Math对象的扩展
Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法将其先转为数值。

Math.trunc('123.456')
// 123

对于空值和无法截取整数的值,返回NaN

Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN

Math.sign()
Math.sign方法用来判断一个数到底是正数、负数、还是零。
它会返回五种值。
参数为正数,返回+1;
参数为负数,返回-1;
参数为0,返回0;
参数为-0,返回-0;
其他值,返回NaN。

Math.cbrt方法用于计算一个数的立方根

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

Math.cbrt('8') // 2
Math.cbrt('hello') // NaN

6.指数运算符
es6新增了一个指数运算符(**)

2**2//4
2**3//8

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)

let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

数组的扩展

1.Array.from()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from("hello")//['h','e','l','l','o']

2.Array.of()
Array.of方法用于将一组值,转换为数组。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

3.数组实例的find()和findIndex()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1,4,-5,10].find((n)=>n<0)//-5

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

4.数组实例的fill()
fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
["a","b","c"].fill()//[undefiend,undefined,undefined]

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

上面代码表示,fill方法从1号位开始,向原数组填充7,到2号位之前结束。
5.数组实例的entries(),keys()和values()
ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
6.数组实例的includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持。

[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false
[1, 2, NaN].includes(NaN); // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

函数的扩展

1.函数参数的默认值

function log(x,y="world")
{
  console.log(x,y)
}
log("Hello")//Hello world
log("Hello","china")//hello china
log("Hello","")//Hello

2.与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用

Symbol

ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。

let s=Symbol()
typeof s//symbol

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

var s1=Symbol("foo")
var s2=Symbol("bar")

s1//Symbol(foo)
s2//Symbol(bar)

s1.toString()//"Symbol(foo)"
s2.toString()//"Symbol(bar)"
上面代码中,s1和s2是两个Symbol值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
sym // Symbol(abc)

注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();

s1 === s2 // false

// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');

s1 === s2 // false

Symbol值不能与其他类型的值进行运算,会报错。
但是,Symbol值可以显式转为字符串。
另外,Symbol值也可以转为布尔值,但是不能转为数值。

2.作为属性名的Symbol
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

实例:消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,该由含义清晰的变量代替。

function getArea(shape, options) {
  var area = 0;
  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }
  return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串

上面代码中,字符串“Triangle”就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量。

var shapeType = {
  triangle: 'Triangle'
};
function getArea(shape, options) {
  var area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。

const shapeType = {
  triangle: Symbol()
};

3.属性名的遍历
Symbol 作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。
Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
var obj = {};

var foo = Symbol("foo");

Object.defineProperty(obj, foo, {
  value: "foobar",
});

for (var i in obj) {
  console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。
另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

你可能感兴趣的:(JS基础)