1.let和const
let和const共同点:声明的变量仅在块级作用域内有效;不存在变量提升;暂时性死区;不允许重复声明;全局变量将逐步与顶层对象的属性脱钩。
let和const不同点:const一旦声明变量,就必须立即初始化,不能留到以后赋值。
代码示例一:

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

代码示例二:

var a = [];
for (var i = 0; i < 10; i++) {
  var c = i;
  a[i] = function () {
    console.log(c);
  };
}
a[6](); // 9

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

var a = [];
for (var i = 0; i < 10; i++) {
  let c = i;
  a[i] = function () {
    console.log(c);
  };
}
a[6](); // 6

代码示例三:

块级作用域:

function f() { console.log('I am outside!'); }
(function () {
  if(false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }
  f();
}());

上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块,其内部声明的函数皆不会影响到作用域的外部。

function f1() { console.log('I am outside!!!!'); }
(function () {
  if(true) {
    // 重复声明一次函数f
    function f1() { console.log('I am inside!!!!'); }
  }
  f1();   // I am outside!!!!
}());

2.变量的解构赋值

(1)数组的解构赋值(如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错):

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

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

对于Set结构,也可以使用数组的解构赋值。

[a, b, c] = new Set(["a1", "b1", "c1"])
a // "a1"
b // "b1"
c // "c1"

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

let [x, y = 'b'] = ['a']; // x='a', y='b'

(2)对象的解构赋值(只要等号右边的值不是对象,就先将其转为对象。比如undefined和null无法转为对象,所以对它们进行解构赋值,就会报错):

数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

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

对象的解构赋值是下面形式的简写:

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

如果变量名与属性名不一致,必须写成下面这样。

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

对象的解构也可以指定默认值。

var {x, y = 5} = {x: 1};
x // 1
y // 5

下面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行,否则JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。

({x} = {x: 1});

解构赋值代码示例:

// 返回一个数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

3.字符串的扩展

(1)Unicode字符表示法: JavaScript允许采用“uxxxx”形式表示一个字符,其中“xxxx”表示字符的码点。超过0xFFFF的数值(比如u20BB7),将码点放入大括号,就能正确解读该字符。

"\u{20BB7}"
// "?"

(2)codePointAt(),fromCodePoint()

对于需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),codePointAt()能够正确处理,返回一个字符的码点。

var s = '?a';
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

codePointAt()是测试一个字符由两个字节还是由四个字节组成的最简单方法。

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}
is32Bit("?") // true
is32Bit("a") // false

String.fromCodePoint(0x20BB7)
// "?"

(3)正则表达式的u修饰符

--点字符
--Unicode字符表示法
--量词
--i修饰符
--预定义模式:由此可以写出一个正确返回字符串长度的函数。

function codePointLength(text) {
    var result = text.match(/[\s\S]/gu);
    return result ? result.length : 0;
}
var s = "??";
s.length // 4
codePointLength(s) // 2

(4)includes(), startsWith(), endsWith(),repeat(),padStart(),padEnd()

var s = "Hello world!";
s.startsWith("world", 6) // true
s.endsWith("Hello", 5) // true
s.includes("Hello", 6) // false
'x'.repeat(3) // "xxx"
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"

注:endsWith()的行为与includes(), startsWith()两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

(5)正则表达式的y修饰符

ES6为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。它的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始,不同之处在于,g修饰符只确保剩余位置中存在匹配,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

var r = /hello\d/y;
r.sticky // true

(6)模板字符串

传统的JavaScript语言写法:

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

ES6模板字符串:

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

4.数值的扩展

(1)二进制和八进制表示法

ES6提供了二进制和八进制数值的新的写法,分别用前缀0b和0o表示。

0b111110111 === 503 // true
0o767 === 503 // true

(2)Number.isFinite(), Number.isNaN():只对数值有效,非数值一律返回false。
(3)Number.parseInt(), Number.parseFloat():行为完全保持不变,逐步减少全局性方法,使得语言逐步模块化。
(4)Number.isInteger(),Number.EPSILON,Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER,Number.isSafeInteger()

5.数组的扩展

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

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.from()还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

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

Array.of(3) // [3]

6.函数的扩展

(1)函数参数的默认值:

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {
  console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误:

function throwIfMissing() {
  throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}
foo()
//Uncaught Error: Missing parameter

参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行)。

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

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

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真:

(function(a){}).length // 1
(function(a=5){}).length // 0
(function(a, b, c=5){}).length // 2

注:定义了默认值的参数,必须是函数的尾部参数,其后不能再有其他无默认值的参数。这是因为有了默认值以后,该参数可以省略,只有位于尾部,才可能判断出到底省略了哪些参数。

(2)rest参数:

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

 function add(...values) {
      console.log(values);
      let sum = 0;
      for (var val of values) {
        sum += val;
      }
      return sum;
   }
   add(2, 5, 3) 
   // [2,5,3]
   // 10

注:rest参数之后不能再有其他参数,否则会报错。

函数的length属性,不包括rest参数:

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

(3)扩展运算符:
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

// ES6的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);

// ES6的合并数组
[...arr1, ...arr2, ...arr3]

注:扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符。

(4)箭头函数:

function(x, y) { 
    x++;
    y--;
    return x + y;
}
(x, y) => {x++; y--; return x+y}

应用示例:

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
var animal = new Animal()
animal.says('hi')  //animal says hi

并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,它的this是继承外面的,因此内部的this就是外层代码块的this。

箭头函数有几个使用注意点:

函数体内的this对象,绑定定义时所在的对象,而不是使用时所在的对象。
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。
由于this在箭头函数中被绑定,所以不能用call()、apply()、bind()
这些方法去改变this的指向。

7.对象的扩展

(1)属性的简洁表示法:

function f(x, y) {
  return {x, y};
}
// 等同于
function f(x, y) {
  return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}

方法简写:

var o = {
  method() {
    return "Hello!";
  }
};
// 等同于
var o = {
  method: function() {
    return "Hello!";
  }
};

(2)属性名表达式:

ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。

var lastWord = 'last word';
var a = {
  'first word': 'hello',
  [lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

(3)Object.is():用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
Object.is(NaN, NaN) 
// true

(4)Object.assign():用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

为对象添加属性:

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

为属性指定默认值:

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};
function processContent(options) {
  let options = Object.assign({}, DEFAULTS, options);
}

8.Symbol()

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

var s1 = Symbol('foo');
var s2 = Symbol('foo');
typeof s1  // "symbol"
s1 === s2  // false

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

var mySymbol = Symbol();
var a = {
  [mySymbol]: 'Hello!'
};

(3)Symbol.for(),Symbol.keyFor():
Symbol.for()方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2          // true

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。

Symbol.keyFor(s1)  // "foo"
var s3 = Symbol("foo");
Symbol.keyFor(s3)  // undefined

9.Set和Map数据结构

(1)Set(类似于数组,但是成员的值都是唯一的,没有重复的值):

var set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

let set = new Set(['red', 'green', 'blue']);
var array = Array.from(set);
console.log(array);    // ["red", "green", "blue"]

Set数据结构有以下方法:

add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。

(2)set遍历操作:

let set = new Set(['red', 'green', 'blue']);

keys()values()entries():

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue
//等同于
for (let item of set) {
  console.log(item);
}
Set.prototype[Symbol.iterator] === Set.prototype.values  // true

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

forEach():

let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6

数组的map()filter()也可以用于Set结构了:

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// Set {2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// Set {2, 4}

使用Set(),可以很容易地实现并集(Union)和交集(Intersect):

let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
let intersect = new Set([...a].filter(x => b.has(x))); 
// Set {2, 3}

(3)Map(类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键):

var map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

注:只有对同一个对象的引用,Map结构才将其视为同一个键:

var map = new Map();
map.set(['a'], 555); 
map.get(['a']) // undefined

上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。由上可知,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。

Map数据结构有以下属性和方法:

size:返回成员总数。
set(key, value):设置key所对应的键值,然后返回整个Map结构,
    如果key已经有值,则键值会被更新,否则就新生成该键。
get(key):读取key对应的键值,如果找不到key,返回undefined。
has(key):返回一个布尔值,表示某个键是否在Map数据结构中。
delete(key):删除某个键,返回true。如果删除失败,返回false。
clear():清除所有成员,没有返回值。

(4)map遍历操作:
keys()values()entries():

let map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 等同于
for (let [key, value] of map.entries()) {
  console.log(key, value);
// 等同于
for (let [key, value] of map) {
  console.log(key, value);
}
Map.prototype[Symbol.iterator] === Map.prototype.entries  // true

forEach():

let map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);
map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});
// Key: F, Value: no
// Key: T, Value: yes

Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(...)。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

10.Iterator和for...of循环

任何部署了Iterator接口的对象,都可以用for...of循环遍历。
在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。

let arr = [4,5,6,0];
let iter = arr[Symbol.iterator]();
iter.next();   // Object {value: 4, done: false}
iter.next();   // Object {value: 5, done: false}
iter.next();   // Object {value: 6, done: false}
iter.next();   // Object {value: 0, done: false}
iter.next();   // Object {value: undefined, done: true}

对于这三类数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。

for(let i of [4,5,6,0]){
    console.log(i);
} // 4 5 6 0

除此之外,其他数据结构(主要是对象)的Iterator接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

下面是为对象添加Iterator接口的例子:

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

11.Generator()

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

for (let v of hw) {
  console.log(v);
}
// hello
// world

yield*语句:如果yield命令后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器。

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());
let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

12.class

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        console.log(this.type + ' says ' + say)
    }
}
let animal = new Animal()
animal.says('hello') //animal says hello
class Cat extends Animal {
    constructor(){
        super()
        this.type = 'cat'
    }
}
let cat = new Cat()
cat.says('hello') //cat says hello

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return '('+this.x+', '+this.y+')';
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); 
    this.color = color;
  }
  toString() {
    return this.color+' '+super.toString();
  }
}
var colorPoint = new ColorPoint(3,6,'purple');
colorPoint.toString()
"purple (3, 6)"

13.Module(importexport命令只能在模块的顶层,不能在代码块之中)

(1)export命令:
export命令后面,使用大括号指定所要输出的一组变量。

var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};

下面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。

function v1() { ... }
function v2() { ... }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 写法一
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};

(2)import命令:

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

import命令要使用as关键字,将输入的变量重命名。

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

export default class 命令用于指定模块的默认输出。

export default class Person{
  constructor(name, age){
    this.name = name;
    this.age = age;
  }
  say(){
    return `我是${this.name},今年${this.age}岁了。`;
  }
}

import命令后面才不用加大括号,因为只可能对应一个方法。

import Person from './second.js';

export class:

export class Fun {
  constructor(color){
    this.color = color;
  }
  sayHello(){
     console.log(`Hello,${this.color}`);
  }
}
import { Fun } from './second.js';

正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
// 正确
export default 42;
// 报错
export 42;

示例:

var hobby = 'tour';
export default hobby;

import hobby from './second.js';

园中桥
49 声望0 粉丝

愿你眼中总有光芒,活成自己想要的模样。


« 上一篇
微信小程序