一、let 和 const

ES6中新增了letconst用于声明变量, 他们声明的变量都是块级作用域变量. 两者的区别是后者声明的是常量, 声明之后就不可以再改变的了.

1.1 只在当前代码块内有效

letconst声明的变量都是块级作用域的, 只在当前代码块内有效, 且外层代码块不受内层代码块影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function testLetAndConst() {
{
var innerBlockVar = 'outer block var';
const innerBlockConst = 'outer block const';
let innerBlockLet = 'outer block let';
}

var outerBlockVar = 'outer block var';
const outerBlockConst = 'outer block const';
let outerBlockLet = 'outer block let';

// var声明的变量要么是全局作用域, 要么是函数作用域
// innerBlockVar是在函数内声明的, 属于函数作用域, 因此可以在这里访问到
console.log(innerBlockVar);
// 这两个变量属于块级作用域, 由于已经在变量定义的代码块外面了, 所以这两行会报错
// console.log(innerBlockConst);
// console.log(innerBlockLet);

console.log(outerBlockVar);
console.log(outerBlockConst);
console.log(outerBlockLet);


// 外层代码块不受内层代码块影响.
let n = 5;
if (true) {
let n = 10;
// 输出10
console.log(n);
}
//输出5
console.log(n);
}

testLetAndConst();

1.2 const声明常量, 值不能变

const声明的变量是常量, 在声明变量的时候需要立刻赋值, 且值不能够再改变. 需要注意:

  • 如果是基本数据类型, 那么就是他们的数值不能改变.
  • 如果是引用类型, 那么这个变量的值表示的是引用类型的内存地址, 这个内存地址不能变. 但是这个内存地址指向的对象的属性是可以变的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const PI = 3.1415;
// 改变值会报错
// PI = 3;


// 引用类型
const person = {
name: 'jeb',
age: 22
};
// 改变值会报错(值存的是引用的地址)
// person = {};
// 引用的对象的属性可以改变
person.age = 23;

1.3 全局环境的变量对象属性

全局环境的变量对象, 在浏览器环境指的是window对象, 在Node指的是global对象.

  • ES5中顶层全局环境的变量对象的属性和全局变量是等价的.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    window.a = 1;
    // 1
    console.log(a);
    // 2
    a = 2;
    console.log(window.a);

    var b = 1;
    // 1
    console.log(b);
    console.log(window.b);
  • 在ES6中, let, const声明的全局变量不属于全局环境的变量对象的属性.

    1
    2
    3
    4
    5
    6
    7
    var a = 1;
    // 1
    console.log(window.a);

    let b = 1;
    // undefinded
    console.log(window.b);

二、解构赋值

按照一定的模式从数组或者对象中提取值, 对变量进行赋值, 这被称为解构.

2.1 数组的解构

可以使用解构赋值快速地从数组中拿出值赋予变量, 五点重要特性:

  • 如果解构不成功, 那么值为undefined.

  • 如果右边不是可遍历结构(iterator), 那么会报错.

  • 可以为变量设置默认值.

  • 可以使用rest运算符...

  • 只要等号两边模式相同, 左边就会被赋予对应的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 不使用解构赋值, 需要使用下标来获取值
let arr = [1, 2, 3, 4];
let a = arr[0];
let b = arr[1];
// 使用解构可以快速地声明变量并赋值
// c === 1, d === 2
let [c, d] = arr;


// 解构不成功, 值为undefined
let foo = [];
// undefinded
console.log(foo);


// 右边不是可遍历结构, 会报错
let [bar] = 1;
let [bar1] = {};
let [bar2] = null;


// 可以为变量设置默认值, 当解构不成功时赋予变量的值就是默认值
let [x = 1, y = 2] = [];
// 1
console.log(x);
// 2
console.log(y);


// 可以使用rest运算符, 将剩余的变量当做一个数组
let eles = [1, 2, 3, 4];
let [e1, ...e2] = eles;
// 1
console.log(e1);
// [2, 3, 4] 是一个数组
console.log(e2);


// 模式匹配, 两边模式相同即可获得值
let [p1, [[p2], p3]] = [1, [[2], 3]];
// 1
console.log(p1);
// 2
console.log(p2);
// 3
console.log(p3);

2.2 对象的解构

解构也可以用于从对象中获取属性值. 注意点:

  • 对象解构赋值是根据属性名来赋值的(不像数组是按次序的), 要获取的变量的名字应该跟相应的属性名相同.
  • 如果想重命名变量的名字(变量名取新的名字, 不跟属性名相同), 那么使用:隔开.
  • 对象解构可以用于嵌套解构的对象.
  • 可以使用rest运算符...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let person = {
name: 'jeb',
age: 22
};
// name和age的顺序调换, 说明是根据属性名来获取的
// 想要获取name这个属性, 但是为存储这个属性值的变量取名为newName, 而不是使用name这个变量名.
let {age, name: newName} = person;


// 解构用于嵌套对象
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
// 'Hello'
console.log(x);
// 'World'
console.log(y);


// 可以使用rest运算符, 将剩余的属性当成一个对象.
let jeb = {
name: 'jeb',
age: 22,
gender: 'male'
};
let {name, ...jebCopy} = jeb;

2.3 函数参数的解构赋值

参数中使用到解构声明的函数在调用的时候会自动将实参进行解构传入函数体中执行.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 解构数组的函数参数
function add([x, y]) {
return x + y;
}
// 3
console.log(add([1, 2, 3]));

// 解构对象的函数参数
function add({x, y}) {
return x + y;
}
//
console.log(add({x: 1, y: 2}));

2.4 解构的用处

2.4.1 从函数返回多个值

有了数组解构后, 可以将多个值放在数组里面返回, 然后使用解构来获取这些值.

1
2
3
4
5
6
7
8
function example() {
let a = 1;
let b = a + 2;
let c = a + b + 2;

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

2.4.2 提取JSON数据

解构赋值可以很方便地取出对象中的数据.

1
2
3
4
5
6
7
8
let jsonData = {
"name": "jeb",
"age": 22
};

let {name, age} = jsonData;
console.log(name);
console.log(age);

三、语言扩展

3.1 字符串的扩展

3.1.1 模板字符串

​ 传统方式下的字符串都是在一行的, 对于一个很长的字符串要换行的话只能使用+号进行连接. ES6模板字符串是增强版的字符串, 使用(`)标识. 它具有如下特性:

  • 可以充当普通字符串.
  • 可以用来定义多行字符串(所有换行和空格都会保存在输出中).
  • 可以在字符串中嵌入表达式${expression}.
1
2
3
4
5
6
7
8
9
10
11
12
// 普通字符串
let normalStr = `In JavaScript '\n' is a line-feed.`;

// 多行字符串
let multiLineStr = `In JavaScript this is
not legal.`;
console.log(`string text line 1
string text line 2`);

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

3.1.2 标签模板

模板字符串跟在一个函数后面, 那么这个函数会被调用来处理这个模板字符串, 这被称为标签模板功能. 如果模板字符串里面有变量, 那么会将模板字符串先处理成多个参数, 然后再调用函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alert`abc`;
// 等同于
alert('abc');

// 例子
function sayHello(name) {
console.log(`hello ${name}.`);
}
// hello alpha.
sayHello`alpha`;


// 将模板字符串处理成多个参数
let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

3.2 函数的扩展

3.2.1 函数参数默认值

ES6之前不能为函数的参数提供默认值. 在ES6中可以将值直接写在参数定义的后面为一个函数的参数定义默认值.

1
2
3
4
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}

3.2.2 rest参数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(...values) {

// 数组 [2, 5, 3]
console.log(values);

let sum = 0;

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

return sum;
}

add(2, 5, 3) // 10

3.2.3 箭头函数

ES6中使用=>来定义箭头函数. 注意点:

  • 如果箭头函数的代码块部分有多于一条语句, 那么使用大括号括起来, 使用return语句返回.
  • 如果只有一条语句, 且这条语句是一个值, 那么不需要大括号, 不需要return, 这个语句的值会自动当成返回值返回.
  • 注意: 如果只有一条语句, 但是这条语句是一个对象(即该函数返回这个对象), 那么需要用小括号将这个对象括起来.
  • 函数体内的this是定义时所在作用域的this对象, 而不是使用该函数时所在的对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 多条语句, 需要加上大括号
let printAndCal(num1, num2) => {
console.log('num1: ', num1);
console.log('num2: ', num2);

return num1 + num2;
}


// 只有一条语句, 不需要加大括号
let cal = (num1, num2) => num1 + num2;


// 只有一条语句, 且为对象, 需要使用小括号包起这个对象
let getItem = id => ({id: id, name: 'name'});
// 没有小括号会报错, 因为大括号被解释为代码块.
// let getItem = id => ({id: id, name: 'name'});


// 注意this的使用
// 这里会输出undefined, 因为是在全局作用域中定义这个函数的, this指向全局变量{}
let person = {
name: 'jeb',
sayName: () => console.log(this.name)
};
person.sayName();
// 这里会输出alpha
function f() {
let person = {
name: 'jeb',
sayName: () => console.log(this.name)
};

person.sayName();
}
f.call({name: 'alpha'});

3.2.4 双冒号运算符

当我们想为一个函数绑定this对象的时候通常会使用call, apply, bind方法. 现在我们可以使用::运算符来取代这些方法了. 双冒号运算符::左边是一个对象, 右边是一个函数, 运算符会自动将左边的对象作为上下文环境this绑定到函数上面.

1
2
3
4
5
6
7
foo::bar;
// 等同于
bar.bind(foo);

foo::bar();
// 等同于
bar.call(foo);

3.3 对象的扩展

3.3.1 扩展运算符

对象的扩展运算符...用于取出参数对象的所有可遍历属性, 拷贝到当前对象之中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let jeb = {name: 'jeb', age: 22};
let jebCopy = {...jeb};


let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);


// name属性会覆盖previousVersion对象的同名属性.
let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};

四、对象新增方法

4.1 Object.is()

ES5中可以通过=====来进行值想等的比较, ES6中新增了Object.is()方法. Object.is(1, 1);

4.2 Object.assign()

Object.assign()用于将源对象的所有可枚举属性赋值到目标对象.

1
2
3
4
5
6
7
const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

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

五、Set和Map数据结构

5.1 Set

Set的成员是唯一的, 初始化的时候可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数.

Set的属性:

  • Set.prototype.constructor
  • Set.prototype.size: 返回Set实例的成员总数.

Set的方法:

  • add(value): 添加某个值, 返回 Set 结构本身.
  • delete(value): 删除某个值, 返回一个布尔值, 表示删除是否成功.
  • has(value): 返回一个布尔值, 表示该值是否为Set的成员.
  • clear(): 清除所有成员, 没有返回值.
  • keys(): 返回键名的遍历器.
  • values(): 返回键值的遍历器.
  • entries(): 返回键值对的遍历器.
  • forEach(): 使用回调函数遍历每个成员.

5.2 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const map = new Map();
map.set('name', 'jeb');
map.set('age', 22);

// 2
console.log(map.size);

// 'jeb'
map.get('name');
// 22
map.get('age');

// true
map.has('name');

// true 成功true, 失败false
map.delete('name');
// false 已经删除了, 再次删除会失败, 返回false
map.delete('false');

// undefinded
map.clear();

// keys() 键名的遍历器
for (let key of map.keys()) {
console.log(key);
}

// values() 键值得遍历器
for (let value of map.values()) {
console.log(value);
}

// entries() 所有成员的遍历器
for (let item of map.entries()) {
console.log(item[0], item[1]);
}

// forEach() 遍历所有成员
map.forEach((value, key, map) => console.log('key: ', key, ' value: ', value, ' map: ', map));

六、Promise对象

Promise是一个容器, 里面保存着初始化Promise对象时传入的executor函数的操作状态, 如果我们为Promise对象添加相应的状态处理器, 那么当状态发生变化的时候会触发相应处理器的异步执行. 共有三种状态: pending, fulfilledrejected. 状态的变化只有两种可能: 从pendingfulfilled和从pendingrejected, 一旦一个Promise对象的状态改变了, 那么他就固定了, 不能够再次改变.

6.1 基础语法

基础语法使用包括:

  • 使用带有resolvereject两个参数的executor函数初始化Promise对象.
  • executor函数中调用resolve方法或者reject方法改变当前Promise对象的状态.
  • 使用Promise.prototype.then()Promise对象添加onfulfilled, onrejected处理器.
  • 使用Promise.prototype.catch()Promise对象添加onrejected异常处理器.
  • 使用Promise.prototype.finally()来执行无论什么情况下都会执行的代码.
  • 使用Promise.all()方法将多个Promise实例包装成一个新的Promise实例.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 传入executor函数初始化Promise对象
let promise = new Promise((resolve, reject) => {

try {
let counter = 0;
for (let i = 0; i < 100; i++) {
counter++;
}

// 将当前Promise对象的状态更改为fulfilled
// resolve(counter);

reject('just want to reject.');

} catch (e) {
reject();
}
});

promise.then((value) => {

console.log('after execute, result is: ', value);
}, (reason) => {

// reject或者抛异常会进入reject回调这里
console.log('execute was rejected, reason is: ', reason);
}).finally(() => {
console.log('finally...');
});

// 当then中没有定义onrejected处理器的时候catch才会起作用,
// promise.then(value => {
//
// console.log('after execute, result is: ', value);
// }).catch(reason => {
//
// console.log('execute was rejected, reason is: ', reason);
// });


// Promise.all()
// p1, p2, p3都是Promise实例
// 只有当这三个的状态都变成fulfilled, p的状态才会变成fulfilled. 此时p1, p2, p3的返回值会组成一个数组, 传递给p的回调函数
// 只要p1, p2, p3中有一个变成了rejected, p的状态就会变成rejected, 此时第一个被rejected的实例的返回值会被传递给p的回调函数
const p = Promise.all([p1, p2, p3]);

6.2 注意点

  • 定义一个Promise对象的时候, 传给它的executor的代码是立即执行的.
  • executor函数里面, 要注意: 调用resolve或者reject之后, 他们后面的代码还是会执行的, 所以一般在调用这两个方法前面加上return.
  • 无论是调用reject函数还是抛出异常, 状态都会变成onrejected.
  • Promise.prototype.catch方法相当于.then(null, rejection), 也就是说仅当没有在then中指定onrejected处理器的时候catch才起作用.
  • 可以使用then链:
    • thenresolve函数里面返回一个值的话, 那么这个值会作为下一个thenresolve的值.
    • 如果返回的是一个Promise对象的话, 那么这个then就是跟在这个Promise对象之后的.
    • 如果没有返回值, 那么继续下一个then.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// new Promise对象的时候, 会立即执行传入的函数.
let promise = new Promise((resolve, reject) => {

try {
let uuid = Math.floor(Math.random() * 100);
if (uuid < 50) {

// 因为调用resolve或者reject之后后面的代码还是会执行, 所以这里加个return, 不然他还会执行reject
// 当promise的状态改变过一次就不能改变了, 这里后面只有一条reject语句, 带来不了什么作用, 但是还是写个return
return resolve(uuid);
}

reject(uuid);
} catch (e) {
// 这里没有调用reject函数, 但是他也是会将状态设置为rejected的(意思是如果会让onrejected处理器来处理或者让catch来处理).
throw new Error('error occurs.');
}
});

promise.then(value => {
console.log('uuid is: ', value);
}).then(value => {
console.log('前面一个then没有返回值, 所以这里value是undefined. value: ', value);
return 'I am value';
}).then(value => {
console.log('前一个返回一个字符串, 值传进来了这里. value: ', value);
return new Promise((resolve, reject) => {
resolve('i am from promise, and resolved.');
});
}).then(value => {
console.log('前一个返回了一个Promise, 那么当这个Promise的状态变为fulfilled之后才会调用这个then. value: ', value);
}).catch(reason => {
console.log('onrejected, reason is: ', reason);
});

七、async, await

asyncawait是一种特殊的语法, 他们和Promise协同工作. async表示函数里有异步操作, await表示紧跟在后面的表达式需要等待结果.

7.1 async函数

使用关键字async修饰的函数执行的时候总是返回一个Promise对象, 而我们在函数代码里面写的返回值则会被包装成Promise的resolved的value.

1
2
3
4
5
6
7
8
async function asyncFunc() {

return 'Hello world!';
}
// 这个promise变量是个Promise对象, 而不是'Hello world!'.
let promise = asyncFunc();
// asyncFunc的返回值在这里传进来.
promise.then(value => console.log(value));

7.2 await

关键字await会让JavaScript等到Promise完成后(fulfilled)才会返回结果. 它后面跟的是Promise对象或者是普通值, 如果是普通值则返回这个值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require('fs');

const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};

const asyncReadFile = async function () {
// 这里会等待两个promise完成
const f1 = await readFile('F:\\Laboratory\\React\\react-doc-study-app\\src\\file1.js');
const f2 = await readFile('F:\\Laboratory\\React\\react-doc-study-app\\src\\file2.js');

return [f1.toString(), f2.toString()];
};

asyncReadFile().then(value => {
console.log(value);
});

7.3 错误处理

如果一个await后面的Promise的状态变为rejected(调用reject方法或者抛出异常), 那么这个async函数停止后面语句的运行, 抛出异常. 如果我们为这个async函数定义了catch回调函数的话会由这个回调函数进行处理.

1
2
3
4
5
6
7
8
9
10
11
12
async function f() {

let a1 = await new Promise((resolve, reject) => {
reject('call the reject method.');
// throw new Error('throw new Error.');
});

// 这个语句是不会执行的.
console.log('await语句对应的Promise变为rejectd状态, 后面的语句不会执行..');
}
// 上面await或者throw语句导致那个promise变成了rejected状态, 所以在这里处理.
f().catch(reason => console.log(reason));

7.4 注意

  • 如果一条await发生了异常, 但是我们希望这个异常不会影响后面语句的执行, 那么可以将它放到try...catch里面.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    async function f() {

    let a1 = 0;
    try {
    a1 = await new Promise((resolve, reject) => {
    reject('call the reject method.');
    });
    } catch (e) {
    // 输出 call the reject method.
    console.log(e);
    }

    // 这个语句是会执行的.
    console.log('因为那个await语句放到了try块里面, 所以这里还会执行.');
    }
    // 这里的catch处理器就没有用到了.
    f().catch(reason => console.log(reason));
  • await作用于promise.then返回的promise的两种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 第一种情况
    async function f() {

    let promise = new Promise((resolve, reject) => {
    resolve('resolved...');
    });

    // 这里then里面的函数返回了一个值, await返回的值就是这个值
    let newPromise = promise.then(value => [value, 'new value']);
    let [value, newValue] = await newPromise;
    // 'resolved' 'new value'
    console.log(value, newValue);
    }

    f();


    // 第二种情况
    async function f1() {

    let promise = new Promise((resolve, reject) => {
    resolve('resolved...');
    });

    let newPromise = promise.then(value => new Promise((resolve, reject) => {
    resolve('hello');
    }));
    let value = await newPromise;
    // hello
    console.log(value);
    }

    f1();

八、Class

8.1 基本语法

  • 使用class关键字定义类.
  • 构造方法为constructor(): 通过new命令生成对象实例时, 自动调用该方法. 一个类如果没有显示定义这个方法, 那么一个空的constructor方法会被创建. 这个方法默认返回this.
  • 定义属性.
  • 定义方法.
  • 取值函数getter, 设值函数getter: 这两个函数会被自动设置到属性的描述符对象的getset上面.
  • 定义静态属性.
  • 定义静态方法.
  • 继承extends.
  • this表示实例对象.
  • super
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Person {

// 可以省略不写
// name;

constructor(name, age) {
// 实例属性
this.name = name;
this._age = age;
// 为方法绑定this对象
this.sayName = this.sayName.bind(this);
}

// 定义方法
sayName() {
console.log(this.name);
}

// 取值器
get age() {
return this._age;
}

// 存值器
set age(value) {
this._age = value;
}

// 定义静态方法
static staticMethod() {
console.log('..static method.');
}
}

Person.staticField = 'static field.';

class Teacher extends Person {

constructor(name, age) {
// 如果子类不使用默认的无参构造方法, 而是重写自己的的构造方法,
// 那么必须在第一行使用super调用父类的构造方法
super(name, age);
}
}

let person = new Person('person', 22);
console.log(person.name);
// person.age是使用了设置器的
console.log(person.age);

// 使用静态属性
console.log(Person.staticField);
// 使用静态方法
Person.staticMethod();


let teacher = new Teacher('teacher', 22);
// 使用了继承来的属性
console.log(teacher.name);
console.log(teacher.age);

8.2 类和继承的原理

​ ES6中新增的class是一个语法糖, 它只是让对象原型的写法更加清晰, 更加接近面向对象编程的语法而已, 使用ES6定义的类的所有属性都存在于类的.prototype属性中, 在类的实例中调用方法其实就是调用实例内部指针指向的原型对象上的方法, 也就是类的.prototype属性指向的对象上的方法.

8.3 注意事项

8.3.1 Class.prototype

ES6的class本质上只是一个语法糖, 所以ES5中构造函数的prototype属性在ES6的类上面也是存在的, 它等同与类的prototype属性. 类的所有方法都定义在类的prototype属性上面. 类的prototype属性对应的对象的constructor属性也是指向类本身, 和ES5中的行为是一致的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor() {

}

sayName() {

}
}

// class只是语法糖, 本质上还是function
console.log(typeof Person);
// Person {} 这里并没有属性, 大概是这个对象内部还有某种数据结构
console.log(Person.prototype);
// [Function: sayName]
console.log(Person.prototype.sayName);
// true
console.log(Person.prototype.constructor === Person);

8.3.2 静态属性和静态方法

​ 在类中定义的属性是放在类的prototype属性上的, 类的所有实例都可以访问, 从上一节可以了解到. 但是如果我们想通过类来调用一个属性或者方法, 在ES5中我们是直接定义在构造函数上, 在类中我们使用static关键字(属性不能使用static关键字, 而是使用类名.staticProName的方式定义).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {

//静态方法
static printTag() {
console.log(this.tag);
}

constructor(name) {
this.name = name;
this.sayName = this.sayName.bind(this);
}

sayName() {
console.log(this.name);
}
}
// 目前ES6静态属性只能这样写
Person.tag = 'person';

let person = new Person('jeb');
person.sayName();
// 静态方法定义在类上面, 而不是类实例的原型对象上面, 所以报错
// person.printTag();

// 调用静态方法
Person.printTag();

8.3.3 this的指向

类方法内部如果含有this, 它默认指向类的实例. 但是注意:

  • 如果将这个方法赋值给另一个对象, 那么this就指向被赋值的对象.

  • 如果将这个方法提取出来单独使用, this会变为undefined(一直以为是指向执行环境window或者global的).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {

constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}

let person = new Person('jeb');
// jeb
person.sayName();


let obj = {
name: 'alpha'
};
obj.sayName = person.sayName;
// this指向obj
// alpha
obj.sayName();


let sayName = person.sayName;
// 报错, 因为this指向undefined了
sayName();

解决办法:

  • 一般在构造方法中将this绑定到方法
  • 或者使用箭头函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person {

constructor(name) {
this.name = name;
// 使用bind
this.sayName = this.sayName.bind(this);

// 或者使用箭头函数
// this.sayName = () => {
// console.log(this.name);
// }
}

sayName() {
console.log(this.name);
}
}

let person = new Person('jeb');
// jeb
person.sayName();


let obj = {
name: 'alpha'
};
obj.sayName = person.sayName;
// jeb
obj.sayName();


let sayName = person.sayName;
// jeb
sayName();

8.3.4 constructor和super

如果子类继承父类且重写了constructor构造方法, 那么在重写的构造方法的第一行必须调用super执行一次父类的构造方法.

九、Module

ES6中的模块化提供了importexport两个关键字. 在ES6, 一个JavaScript文件被视为一个模块.

9.1 export

四种export的方式:

  • export写在变量定义前.
  • 先定义变量, 然后用大括号将变量名括起来export出去.
  • 在大括号里面对变量重命名.
  • export default输出默认变量, 这样在import的时候可以自己定义名字.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//写在变量定义前
export let firstname = 'jeb';
export function sayHello() {
console.log('Hello world!');
}

//使用大括号
let firstname = 'jeb';
function sayHello() {
console.log('Hello world!');
}
export {firstname, sayHello};

//重命名
let firstname = 'jeb';
function sayHello() {
console.log('Hello world!');
}
export {firstname, sayHello as exportedSayHello};

//输出默认值
export default function() {
console.log('Hello, this is default function.');
}

9.2 import

当使用export定义了模块对外的接口后, 既可以通过import命令从这个模块加载内容了. 需要注意两点:

  • 可以使用as来对变量重新取名.
  • 如果export出去的变量是默认变量, 那么这个名字我们可以任意取.
  • 可以使用*来整理加载一个模块导出的接口.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 常规用法
import {firstname, sayHello} from './xxx.js';

// 更改变量名
import {firstname as importedFirstname, sayHello} from './xxx.js';

// 为export的默认变量取名字
import defaultVar from './xxx.js';

// 使用* 整体加载模块
import * as xxxInterface from './xxx.js';
// xxxInterface被当成了一个对象, export出来的接口称为了属性, 使用属性名调用.
console.log('firstname: ', xxxInterface.firstname);
xxxInterface.sayHello();