《ES6标准入门》(第3版作者:阮一峰)读书笔记 19-26章
对国内十分有名的一本书--《ES6标准入门》进行个人的读书总结,对书中的知识点进行实践验证和记录。想要看原版的请购买正版书籍。
传统方法创建新对象
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.personDetail = function (){
console.log(this.name,this.age)
}
var person = new Person('lee',18)
通过Class改写
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
personDetail(){
console.log(this.name,this.age)
}
}
typeof Person // function(看作是构造函数的另一种写法)
Person == Person.prototype.constructor // true
// 使用new创建
var person = new Person('lee',18)
console.log(person) // Person {name: "lee", age: 18}
类的方法全部都定义再类的prototype属性上
console.log(person.__proto__) // {constructor: ƒ, personDetail: ƒ}
所以
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
personDetail(){
console.log(this.name,this.age)
}
}
相当于
Person.prototype = {
construct(){},
personDetail(){}
}
类内部的所有方法都是不可枚举的
Object.keys(Person.prototype) // []
Object.getOwnPropertyNames(Person.prototype) // ["constructor", "personDetail"]
ES5写法时类内部方法是可以枚举的
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.personDetail = function (){
console.log(this.name,this.age)
}
var person = new Person('lee',18)
console.log(Object.keys(Person.prototype)) // ["personDetail"](可以枚举)
console.log(Object.getOwnPropertyNames(Person.prototype)) // ["constructor", "personDetail"]
类属性名可以采用表达式
let methodName = 'personDetail'
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
[methodName](){
console.log(this.name,this.age)
}
}
类和模块内部默认使用严格模式
constructor方法是类的默认方法,通过new命令生成对象实例时自动调用该方法。如果没有显式定义,一个空的constructor方法会被自动添加。
class Person{}
console.log(Person.prototype) // {constructor: ƒ}(自动添加了)
constructor指定另一个返回对象
class Person{
constructor(){
return {}
}
}
new Person() instanceof Person; // false(导致判断出错,所以为什么还要改它?)
当作函数直接调用报错
class Person{
}
Person() // 报错 Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
实例属性除非显示定义在其本身即this对象上,否则都是定义在原型上。
class Person{
constructor(name,age){
this.name = name;
this.age = age;
}
personDetail(){
console.log(this.name,this.age)
}
}
var person = new Person('lee',18);
person.personDetail(); // lee 18
console.log(person.hasOwnProperty('name')) // true(在自身上)
console.log(person.hasOwnProperty('age')) // true(在自身上)
console.log(person.hasOwnProperty('personDetail')) // false(不在自身上)
console.log(person.__proto__.hasOwnProperty('personDetail')) // true(在原型上)
所有实例共享一个原型对象
var person1 = new Person('my',18);
var person2 = new Person('mht',18);
person1.__proto__ === person2.__proto__; // true
使用__proto__修改原型链时,所有实例都会被影响。
person1.__proto__.personDetail = function(){
console.log('我要修改全部原型')
}
person1.personDetail(); // 我要修改全部原型
person2.personDetail();// 我要修改全部原型
Class使用表达式形式
const Man = class Person{
getClassName(){
return Person.name;
}
}
let man1 = new Man();
man1.getClassName(); // "Person"
Person.name // 报错 Uncaught ReferenceError: Person is not defined(已经不能在外部使用)
Person只在Class内部代码可用,指代当前类 如果内部也没有用到那么可以省略Person
const Man = class{
getClassName(){
return this.name;
}
}
let man1 = new Man();
立即执行的Class
const Man = class{
constructor(name){
this.name =name;
}
}
let man1 = new Man('lee');
console.log(man1) // Man {name: "lee"}
new Person();
class Person{
} // Uncaught ReferenceError: Cannot access 'Person' before initialization
加下划线标记私有方法
class Person {
//公有方法
getName(){}
// 私有方法
_getAge(){}
}
将私有方法移出模块
class Person {
_setAge(age){
setAge.call(this,age) // 借用call
}
}
function setAge(age){ // 这是私有方法,,必须通过公有方法访问
return this.age = age
}
var person = new Person();
person._setAge(18);
console.log(person) // Person {age: 18}
利用Symbol值的唯一性将私有方法的名字命名为Symbol值
const bar = Symbol('bar');
const snaf = Symbol('snaf')
export default class myClass{
// 公有方法
foo(baz){
this[bar](baz)
}
// 私有方法
[bar](baz){ // 必须通过上一个方法调用
return this[snaf] = baz;
}
}
略
如果单独提取出来单独使用,this会指向方法运行时的所在环境
class Logger{
printName(name = 'lee'){
this.print(`hi ${name}`)
}
print(text){
console.log(text)
}
}
const logger = new Logger();
logger.printName(); // hi lee(这样调用没问题)
const {printName} = logger // 取出来调用报错
printName(); // Uncaught TypeError: Cannot read property 'print' of undefined
修复一
class Logger{
constructor(){
this.printName = this.printName.bind(this) // 重新指定this指向
}
printName(name = 'lee'){
this.print(`hi ${name}`)
}
print(text){
console.log(text)
}
}
const logger = new Logger();
const {printName} = logger
printName(); // hi lee(没问题了)
修复二
class Logger{
printName = (name = 'lee')=>{
this.print(`hi ${name}`)
}
print(text){
console.log(text)
}
}
const logger = new Logger();
const {printName} = logger
printName(); // hi lee
使用Proxy,获取式自动绑定this
class Logger{
printName = (name = 'lee')=>{
this.print(`hi ${name}`)
}
print(text){
console.log(text)
}
}
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger1 = selfish(new Logger());
const {printName} = logger1
printName(); // hi lee
class Person{};
Person.name // "Person"
class MyClass{
constructor(){}
get prop(){
return 'getter' // 如果return this.prop会导致死循环
}
set prop(value){
console.log(`设置prop属性值为${value}`)
}
}
let inst = new MyClass();
inst.prop = 'lee'; // 设置prop属性值为lee
inst.prop; // "getter"
存值函数和取值函数是设置在属性的desctriptor对象上的
class MyClass{
constructor(state= {}){
this.state = state;
}
get prop(){
return this.state.prop;
}
set prop(value){
this.state.prop = value;
}
}
let inst = new MyClass();
var descriptor = Object.getOwnPropertyDescriptor(MyClass.prototype,"prop")
console.log("get" in descriptor)
console.log("set" in descriptor)
如果某个方法之前加上星号* ,就表示该方法是一个Generator函数。
class MapArr{
constructor(...args){
this.args = args;
}
*[Symbol.iterator](){// 每次调用iterator.next就会调用一次该函数,如果是Generator函数更好一一对应
for(let arg of this.args){
yield arg;
}
}
}
var arr = new MapArr(1,2,3,4,5);
console.log([...arr]) // [1, 2, 3, 4, 5]
static关键字表示该方法不会被实例继承而是直接通过类调用
class MyClass{
static classMethod(){
console.log('hello')
}
}
MyClass.classMethod() // hello(类调用)
var inst = new MyClass();
inst.classMethod(); //报错 Uncaught TypeError: inst.classMethod is not a function(实例上不可访问)
但是父类的静态方法可以被子类继承,同样需要类调用。
class Child extends MyClass{
}
Child.classMethod() // hello
子类也可以在super对象上调用
class Child2 extends MyClass{
static classMehthod(){
return super.classMethod()
}
}
Child2.classMethod(); // hello
class Foo{}
Foo.prop = 1;
Foo.prop; // 1(Class本身属性)
Class内部只有静态方法没有静态属性
Class的实例属性可以用等式写入类的定义之中。
class MyClass{
myProp = 42;
constructor(){
console.log(this.myProp)
}
}
var myClass = new MyClass(); // 输出 42
class MyClass{
static myProp = 42;
}
console.log(MyClass.myProp) // 42(静态属性)
旧写法: MyClass.prop = 1;
构造函数不是使用new命令调用的,那么new.target会返回undefined,是使用new命令调用的话会返回函数名
function Person(name){
if(new.target !== undefined){
this.name = name;
}else{
throw new Error('必须使用new生成实例')
}
}
var person1 = new Person('lee');
var person2 = Person.call(person1,'lee') // 报错 Uncaught Error: 必须使用new生成实例
另一种写法
function Person(name){
if(new.target === Person){
this.name = name;
}else{
throw new Error('必须使用new生成实例')
}
}
var person1 = new Person('lee');
var person2 = Person.call(person1,'lee') // 报错 Uncaught Error: 必须使用new生成实例
Class可以通过extends关键字实现继承
class Person{}
class HeightPerson extends Person{}
class Person{
constructor(name,age){
this.name = name; // 名字
this.age = age; // 年龄
}
toString(){
return `他叫${this.name},年龄${this.age}`
}
}
class Basketballplayer extends Person{
constructor(name,age,height){
super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
this.height = height; // 篮球运动员的身高
}
toString(){
return super.toString() + `,身高为${this.height}的篮球运动员`
}
}
var Lee = new Basketballplayer('lee','18',195)
Lee.toString(); // "他叫lee,年龄18,身高为195的篮球运动员"
子类必须在constructor方法中调用super,super表示父类的构造函数,用来创建新的this对象,否则会报错。 ES5的继承实质是先创造子类的实例对象this,然后在将父类的方法添加到this上面(Parent.apply(this))。ES6实质是先创造父类的实例对象(所以必须先调用super方法),然后再用子类的构造函数修改this。 如果子类没有定义constructor方法,那么这个方法会被默认添加。
class Basketballplayer extends Person{
}
等同于
class Basketballplayer extends Person{
constructor(...args){
super(...args)
}
}
只有调用了super之后才能使用this关键字
class Person{
constructor(name,age){
this.name = name; // 名字
this.age = age; // 年龄
}
}
class Basketballplayer extends Person{
constructor(name,age,height){
this.height = height; // 篮球运动员的身高
super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
}
}
var Lee = new Basketballplayer('lee','18',195) // 报错 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
实例对象lee同时是Person和Basketballplayer两个类的实例
class Person{
constructor(name,age){
this.name = name; // 名字
this.age = age; // 年龄
}
}
class Basketballplayer extends Person{
constructor(name,age,height){
super(name,age) // 调用父类的constructor(name,age)人的名字和年龄等信息
this.height = height; // 篮球运动员的身高
}
}
var lee = new Basketballplayer('lee',18,195)
console.log(lee instanceof Person)
console.log(lee instanceof Basketballplayer)
接上例
Object.getPrototypeOf(Basketballplayer) //从子类获取父类
// class Person{...}
super这个关键字既可以当作函数使用,也可以当作对象使用。 情况一: 作为函数调用时代表父类的构造函数,子类必须执行一次super函数。
class A{};
class B extends A{
constructor(){
super(); // 必须调用
}
}
super代表父类A的构造函数,但是内部的this指向的是B。相当于A.prototype.constructor.call(this)
class A{
constructor(){
console.log(new.target.name)
}
};
class B extends A{
constructor(){
super();
}
}
new A(); // A
new B(); // B
super函数只能在子类构造函数中使用,其他地方会报错。
class A{};
class B extends A{
m(){
super();
}
}
new B(); // Uncaught SyntaxError: 'super' keyword unexpected here
情况二:作为对象时在普通方法中指向父类的原型对象,静态方法中指向父类。
class A{
p(){
return 2;
}
};
class B extends A{
constructor(){
super();
console.log(super.p())
}
}
new B(); // 2
相当于调用了A.prototype.p() super指向了父类的原型对象,父类实例上的属性和方法无法通过super调用,但是定义在原型上的属性和方法可以。
class A{
constructor(){
this.p = 2;
}
};
class B extends A{
get m(){
return super.p;
}
}
let b = new B();
b.m // undefined
定义在原型上就可以获取了
class A{};
A.prototype.p = 2;
class B extends A{
get m(){
return super.p;
}
}
let b = new B();
b.m // 2
ES6规定,super方法调用父类方法时,super会绑定子类的this
class A{
constructor(){
this.x = 1;
}
print(){
console.log(this.x)
}
};
class B extends A{
constructor(){
super();
this.x = 2;
console.log(super.print())
}
}
let b = new B(); // 2(绑定子类的this)
相当于调用super.print.call(this) constructor中super指向子类本身this,普通方法中super指向父类原型,即拥有自身的属性和共用方法。
子类的_proto_属性代表构造函数继承,子类的prototype的_proto_属性代表方法继承。
class A{};
class B extends A{}
console.log(B.__proto__ === A);
console.log(B.prototype.__proto__ === A.prototype);
回忆原型链person.__proto__ === Person.prototype
略
子类原型的原型是父类的原型
class A{};
class B extends A{}
var a = new A();
var b = new B();
b.__proto__.__proto__ == a.__proto__; // true
略
CommonJS和AMD都是只能运行时确定模块依赖关系。CommonJS模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { a, b, c} = require('fs');
相当于
// CommonJS模块
let _fs = require('fs'); // 会把整个模块都加载进来
let { a,b,c} = _fs
ES6模块的设计思想是尽量静态化,使得编译的时候就能确定模块的依赖关系,以及输入输出的变量。
// ES6模块
import {a,b,c} from 'fs' // 只加载a,b,c这三个方法
ES6模块自动采用严格模式。
模块功能主要由两个命令构成:export和import。即导出和导入。 export的写法一
export var a = 'a';
export var b = 'b';
export var c = 'c'; // 一个个导出时
写法二
var a = 'a';
var b = 'b';
var c = 'c';
export {a,b,c} // 要用花括号括起来,因为不止导出一个
输出类和函数
export function add(a,b){return a+b}
使用关键字as改变名称
function add(a,b){return a+b}
export {add as add1}
export 1 // 报错 1 不是接口
export 语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部的实时值。
export var foo = 1;
setTimeout(()=>foo = 2,500)
先是输出1,再变成2 export语句放在函数中报错
function add(){
export default 'a'
} // 报错
使用import命令加载模块。
import {a,b} from './add.js'
function add(){
return a + b
}
取别名
import {a as a1} from './add.js'
import命令具有提升效果,会提升到整个模块的头部并首先执行
func();
import {func} from './add.js'
静态执行,不能使用变量和表达式
import {a+'1'} from './index.js' // 报错
let module = 'index.js'
import {a+'1'} from module // 报错
if(x==1){
import {a+'1'} from index.js' // 报错
}else{
import {a+'1'} from index1.js' // 报错
}
import语句会执行所加载模块
import 'lodash' // 执行
import 语句是单例模式
import {foo} from './index.js'
import {bar} from './index.js'
等同于
import {foo, bar} from './index.js'
整体加载
import * as allFunc from './index.js'
不允许下面操作
circle.foo =1;
circle.area = function(){}
export default指定默认输出
export default function(){}
其他文件加载时
import func from './index.js'
导出非匿名函数
export default function foo(){}
或者
function foo(){}
export default foo;
导入时取别名
import {default as default1} from './index.js'
下面操作错误
export default var a = 1; // 错误
下面操作正确
export default 42; // 正确
同时有export和export default时,导入写法如下
import func,{a,b,c} from './index.js' //funct为默认输出,其他为普通导出
其实本质是把default后面的值赋给default
导入后又导出
export {foo,bar} from './index.js'
使用别名
export {foo as foo1,bar} from './index.js'
整体导入又整体输出
export * from './index.js'
默认接口导入又导出
export {default} from './index.js'
具名接口改为默认接口
export {foo as default} from './index.js'
默认接口改为具名接口
export {default as foo} from './index.js'
export * from './index.js'
export default function(){
return 1
}
export * 命令会忽略index.js模块的default 方法 改变属性或者方法名后输出
export {a as a1} from './index.js
加载上面模块
import * as all from './main.js;
import def from './main.js'
多个常量的导出导入
export const a = 1;
export const b = 2;
export const c = 3;
导入
import * as all from './index.js'
console.log(all.a) // 1
console.log(all.b) // 2
另一种导入方式
import {a,b} from './index.js'
console.log(a) // 1
console.log(b) // 2
将多个文件集合后再输出
export const a = 1;
export const b = 2;
集合导出两个文件
export {a} from './index.js'
export {b} from './index1.js'
导入
import {a,b} from './main.js'
按需加载
import('./index.js').then(()=>{})
条件加载
if(condition){
import('./index').then(...)
}
动态的模块路径
import(f()).then(...)
html页面中,浏览器通过<script>
标签来加载脚本
内嵌脚本
<script type="application/javascript">
// 脚本代码
</script>
外部脚本
<script type="application/javascript" src="./index.js"></script>
type属性可以忽略 浏览器同步加载js脚本,渲染引擎遇到script标签就会停下来,必须等待脚本执行完毕,外部脚本还需加入脚本下载时间。 防止下载时间过长导致的浏览器卡死,一般使用浏览器脚本的异步加载。 defer: 等到浏览器渲染结束才执行
<script type="application/javascript" src="./index.js" defer></script>
async: 一旦下载完成就终断渲染,来执行。多个async不能保证其执行顺序。
<script type="application/javascript" src="./index.js" async></script>
浏览器ES6模块使用script标签,使用type属性,具体为type="module"
<script type="module" src="./index.js"></script>
异步加载相当于
<script type="module" src="./index.js" defer></script>
使用async属性也是完全可以的,会中断浏览器渲染,执行完毕后再渲染
<script type="module" src="./index.js" async></script>
也允许内嵌在网页内部,相当于加载外部脚本
<script type="module">
// 你的代码
</script>
差异: 1、CommonJS模块是对值的复制,ES6模块输出是对值的引用。 2、CommonJS模块是运行时加载,ES6模块是编译时输出接口。 ES6模块对值的更改会动态反映到所有的引入文件,引入的参数只读不可赋值,除非是对象则可以更改属性。不同脚本加载同一个模块得到的都是同一个实例。 CommonJS模块
// lib.js
var a = 1
module.exports = {
a: a
}
// main.js
var mod = require('./lib.js')
Node加载中,一个模块脚本只要一行import或export语句,node就会认为这个脚本为ES6模块,否则为commonjs模块。如果不输出任何模块但是想要被node认为是ES6模块,可以加一句 export {};
一个CommonJS模块
// a.js
module.exports = {
foo: 1
}
等同于
export default {
foo: 1
}
使用import命令加载
import all from './a'
// all = {foo: 1}
或者
import {default as all} from './a'
// all = {foo: 1}
CommonJS模块的输出缓存机制在ES6加载方式下依然有效
module.exports = 123 // 缓存机制导致导出将会一直是123,不会变为null
setTimeout(_=>module.exports = null)
使用星号导入的例子
module.exports = null
import * as all from './b'
// all = {default : null}(全导入时会自动添加default属性)
采用require加载ES6模块
// es.js
let foo = {bar : 1}
export default foo;
foo = null
// require.js
const es_namespace = require('./es')
console.log(es_namespace.default) // {bar: 1}(存在缓存机制,不会变为null)
容易出现互相依赖的情况
node内部加载模块后会生成一个对象,需要用到时就会到exports属性上取,即使再次require加载也不会再次执行模块,而是到缓存中读取。 略
引入时会停下来执行另一个脚本,另一个文件再引入前一个脚本时,即前一个脚本未执行完毕时只返回执行部分。 略
如果a.js和b.js互相加载,执行a.js 由于 a.js的第一行是加载 b. js,所以先执行的是 b.js。而 b.js的第一行又是加载 a.js ,这时由于 a.js已经开始执行,所以不会重复执行,而是继续执行 b.js,b.js中输出a.js引入的值为undefined,因为a.js没执行完毕。简而言之,就是不会重复执行。
略