5、Python3 ES6 Javascript 进阶

作者: Brinnatt 分类: python 道 发布时间: 2023-05-21 09:48

JavaScript 是一种基于原型(Prototype)的面向对象语言,而不是基于类的面向对象语言。

C++、Java 有类 Class 和实例 Instance 的概念,类是一类事物的抽象,而实例则是类的实体。

JS 是基于原型的语言,它只有原型对象的概念。原型对象就是一个模板,新的对象从这个模板构建从而获取最初的属性。任何对象在运行时可以动态的增加属性。

而且,任何一个对象都可以作为另一个对象的原型,这样后者就可以共享前者的属性。

5.1、定义类

5.1.1、字面式声明方式

var obj = {
    property_1: value_1, // property_# may be an identifier...
    property_2: value_2, // or a number...
    ...,
    "property n": value_n // or a string
};

这种方法也称作字面值创建对象。Js 1.2 开始支持。

5.1.2、构造器(ES6 之前)

1、定义一个函数(构造器)对象,函数名首字母大写。

2、使用 this 定义属性。

3、使用 new 和构造器创建一个新对象。

// 定义类
function Point(x, y) {
    this.x = x;
    this.y = y;
    this.show = () => console.log(this, this.x, this.y);
    console.log('Point~~~~~~~~');
}
console.log(Point);
p1 = new Point(4, 5);
console.log(p1);
console.log('----------------');

// 继承
function Point3D(x, y, z) {
    Point.call(this, x, y); // "继承"
    this.z = z;
    console.log('Point3D~~~~~~~~');
}

console.log(Point3D);
p2 = new Point3D(14, 15, 16);
console.log(p2);
p2.show();

new 构建一个新的通用对象,new 操作符会将新对象的 this 值传递给 Point3D 构造器函数,函数为这个对象创建 z 属性。

从上句话知道,new 得到一个对象,使用这个对象的 this 来调用构造器,那么如何执行 “基类” 的构造器方法呢?

使用 Point3D 对象的 this 来执行 Point 的构造器,所以使用 call 方法,传入子类的 this。

最终,构造完成后,将对象赋给 p2。

注意:如果不使用 new 关键字,就是一次普通的函数调用,this 不代表实例。

5.1.3、class(ES6)

从 ES6 开始,新提供了 class 关键字,使得创建对象更加简单、清晰。

1、类定义使用 class 关键字。创建的本质上还是函数,是一个特殊的函数。

2、一个类只能拥有一个名为 constructor 的构造器方法。如果没有显式的定义一个构造方法,则会添加一个默认的 constuctor 方法。

3、继承使用 extends 关键字。

4、一个构造器可以使用 super 关键字来调用一个父类的构造函数。

5、类没有私有属性。

// 基类定义
class Point {
    constructor(x, y) /*构造器*/ {
        this.x = x;
        this.y = y;
    }
    show() /*方法*/ {
        console.log(this, this.x, this.y);
    }
}
let p1 = new Point(10, 11)
p1.show()

// 继承
class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
    }
}
let p2 = new Point3D(20, 21, 22);
p2.show()

5.1.4、重写方法

子类 Point3D 的 show 方法,需要重写:

// 基类定义
class Point {
    constructor(x, y) /*构造器*/ {
        this.x = x;
        this.y = y;
    }
    show() /*方法*/ {
        console.log(this, this.x, this.y);
    }
}
let p1 = new Point(10, 11)
p1.show()

// 继承
class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
    }
    show() { // 重写
        console.log(this, this.x, this.y, this.z);
    }
}
let p2 = new Point3D(20, 21, 22);
p2.show();

子类中直接重写父类的方法即可。

如果需要使用父类的方法,使用 super.method() 的方式调用。

使用箭头函数重写上面的方法。

// 基类定义
class Point {
    constructor(x, y) /*构造器*/ {
        this.x = x;
        this.y = y;
        //this.show = function () {console.log(this,this.x,this.y)};
        this.show = () => console.log('Point');
    }
}

// 继承
class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
        this.show = () => console.log('Point3D');
    }
}
let p2 = new Point3D(20, 21, 22);
p2.show(); // Point3D

从运行结果来看,箭头函数也支持子类的覆盖。

// 基类定义
class Point {
    constructor(x, y) /*构造器*/ {
        this.x = x;
        this.y = y;
        this.show = () => console.log('Point');
    }
    // show() /*方法*/ {
    // console.log(this,this.x,this.y);
    // }
}

// 继承
class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
        //this.show = () => console.log('Point3D');
    }
    show() { // 重写
        console.log('Point3D');
    }
}
let p2 = new Point3D(20, 21, 22);
p2.show(); // Point

上例优先使用了父类的属性 show。

// 基类定义
class Point {
    constructor(x, y) /*构造器*/ {
        this.x = x;
        this.y = y;
        //this.show = () => console.log('Point');
    }
    show() /*方法*/ {
        console.log(this, this.x, this.y);
    }
}

// 继承
class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
        this.show = () => console.log('Point3D');
    }
}
let p2 = new Point3D(20, 21, 22);
p2.show(); // Point3D

优先使用了子类的属性。

总结:

父类、子类使用同一种方式类定义方法,子类覆盖父类。

如果父类使用属性,子类使用方法,则使用父类的属性;如果父类使用方法,子类使用属性,则使用子类的方法。

所以,一句话,优先用属性。

5.1.5、静态方法

在方法名前加上 static,就是静态方法了。

class Add {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    static print() {
        console.log(this.x); // ? this是Add,而不是Add的实例
    }
}

add = new Add(40, 50);
console.log(Add);
Add.print();

//add.print(); // 实例不能访问直接访问静态方法,和C++、Java一致
add.constructor.print(); // 实例可以通过constructor访问静态方法

注意:静态的概念和 Python 的静态不同,相当于 Python 中的类变量。

5.1.6、this 的坑

虽然 Js 和 C++ 、Java 一样有 this,但是 Js 的表现是不同的。

原因在于, C++ 、Java 是静态编译型语言,this 是编译期绑定,而 Js 是动态语言,运行期绑定。

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return function () {
            console.log(this === global); // this是否是global对象
            return this.name;
        };
    }
}
console.log(school.getNameFunc()());

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
true
undefined
/*

为了分析上面的程序,先学习一些知识:

函数执行时,会开启新的执行上下文环境 ExecutionContext。

创建 this 属性,但是 this 是什么就要看函数是怎么调用的了。

1、myFunction(1,2,3),普通函数调用方式,this 指向全局对象。全局对象是 nodejs 的 global 或者浏览器中的 window。

2、myObject.myFunction(1,2,3),对象方法的调用方式,this 指向包含该方法的对象。

3、call 和 apply 方法调用,要看第一个参数是谁。

分析上例:

brinnatt 和 name: 'brinnatt', getNameFunc: [Function: getNameFunc] 很好理解。

第三行打印的 true,是 console.log(this == global) 执行的结果,说明当前是 global,因为调用这个返回的函数是直接调用的,这就是个普通函数调用,所以 this 是全局对象。

第四行 undefined,就是因为 this 是 global,没有 name 属性。

这就是函数调用的时候,调用方式不同,this 对应的对象不同,它已经不是 C++、Java 的指向实例本身了。

this 的问题,这是历史遗留问题,新版只能保留且兼容了。

而我们在使用时,有时候需要明确的让 this 必须是我们期望的对象,如何解决这个问题呢?

5.1.6.1、显式传入

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return function (that) {
            console.log(this == global); // this是否是global对象
            return that.name;
        };
    }
}
console.log(school.getNameFunc()(school));

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
true
brinnatt
*/

通过主动传入对象,这样就避开了 this 的问题。

5.1.6.2、ES3 引入了 apply、call 方法

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return function () {
            console.log(this == global); // this是否是global对象
            return this.name;
        };
    }
}
console.log(school.getNameFunc().call(school)); // call方法显式传入this对应的对象

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
false
brinnatt
*/

apply、call 方法都是函数对象的方法,第一参数都是传入对象引入的。

apply 传其他参数需要使用数组。

call 传其他参数需要使用可变参数收集。

function Print() {
    this.print = function (x, y) { console.log(x + y) };
}
p = new Print(1, 2)
p.print(10, 20)
p.print.call(p, 10, 20);
p.print.apply(p, [10, 20]);

5.1.6.3、ES5 引入了 bind 方法

bind 方法来设置函数的 this 值。

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return function () {
            console.log(this == global); // this是否是global对象
            return this.name;
        };
    }
}
console.log(school.getNameFunc().bind(school)); // bind方法绑定

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
[Function: bound ]
*/

只打印了三行,说明哪里有问题,问题出在 bind 方法用错了。

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return function () {
            console.log(this == global); // this是否是global对象
            return this.name;
        };
    }
}
var func = school.getNameFunc();
console.log(func);
var boundfunc = func.bind(school); // bind绑定后返回新的函数
console.log(boundfunc);
console.log(boundfunc());

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
[Function (anonymous)]
[Function: bound ]
false
brinnatt
*/

apply、call 方法,参数不同,调用时传入 this。

bind 方法是为函数先绑定 this,调用时直接用。

5.1.6.4、ES6 引入支持 this 的箭头函数

ES6 新技术,就不需要兼容 this 问题。

var school = {
    name: 'brinnatt',
    getNameFunc: function () {
        console.log(this.name);
        console.log(this);
        return () => {
            console.log(this == global); // this是否是global对象
            return this.name;
        };
    }
}
console.log(school.getNameFunc()());

/* 运行结果
brinnatt
{ name: 'brinnatt', getNameFunc: [Function: getNameFunc] }
false
brinnatt
*/

ES6 新的定义方式如下:

class school {
    constructor() {
        this.name = 'brinnatt';
    }
    getNameFunc() {
        console.log(this.name);
        console.log(this, typeof (this));
        return () => {
            console.log(this == global); // this是否是global对象
            return this.name;
        };
    }
}
console.log(new school().getNameFunc()());

/* 运行结果
brinnatt
school { name: 'brinnatt' } object
false
brinnatt
*/

以上解决 this 问题的方法,bind 方法最常用。

5.2、高阶对象、高阶类、或称 Mixin 模式

Mixin 模式,混合模式。这是一种不用继承就可以复用的技术。主要还是为了解决多重继承的问题。多继承的继承路径是个问题。

JS 是基于对象的,类和对象都是对象模板。

混合 mixin,指的是将一个对象的全部或者部分拷贝到另一个对象上去。其实就是属性了。

可以将多个类或对象混合成一个类或对象。

5.2.1、继承实现

先看一个继承实现的例子:

class Serialization {
    constructor() {
        console.log('Serialization constructor~~~~~~~~~~~~~');
        if (typeof (this.stringify) !== 'function') {
            throw new ReferenceError('should define stringify.');
        }
    }
}
class Point extends Serialization {
    constructor(x, y) {
        console.log('Point Constructor~~~~');
        super(); // 调用父构造器
        this.x = x;
        this.y = y;
    }
}

// s = new Serialization(); // 构造Serialization失败    
// p = new Point(4,5); // 构造子类对象时,调用父类构造器执行也会失败

父类构造函数中,要求具有属性是 stringify 的序列化函数,如果没有则抛出异常。

以下是完整继承的代码:

class Serialization {
    constructor() {
        console.log('Serialization constructor~~~');
        if (typeof (this.stringify) !== 'function') {
            throw new ReferenceError('should define stringify.');
        }
    }
}

class Point extends Serialization {
    constructor(x, y) {
        console.log('Point Constructor~~~~');
        super(); // 调用父构造器
        this.x = x;
        this.y = y;
    }
    stringify() {
        return `<Point x=${this.x}, y=${this.y}>`
    }
}

class Point3D extends Point {
    constructor(x, y, z) {
        super(x, y);
        this.z = z;
    }
    stringify() {
        return `<Point x=${this.x}, y=${this.y}, z=${this.z}>`
    }
}

p = new Point(4, 5);
console.log(1, '-->', p.stringify())

p3d = new Point3D(7, 8, 9);
console.log(2, '-->', p3d.stringify());

5.2.2、高阶对象实现

将类的继承构建成箭头函数。

// 普通的继承
class A extends Object { };
console.log("普通的继承", '-->', A);

// 匿名类
const A1 = class {
    constructor(x) {
        this.x = x;
    }
}
console.log("匿名类", '-->', A1);
console.log("匿名类", '-->', new A1(100).x);

// 匿名继承
const B = class extends Object {
    constructor() {
        super();
        console.log("匿名继承", '-->', 'B constructor');
    }
};
console.log("匿名继承", '-->', B);
let b = new B();
console.log("匿名继承", '-->', b);

// 箭头函数,参数是类,返回值也是类
// 把上例中的Object看成参数
const x = (Sup) => {
    return class extends Sup {
        constructor() {
            super();
            console.log("箭头高阶", '-->', 'C constructor');
        }
    };
}

// 演化成下面的形式
const C = Sup => class extends Sup {
    constructor() {
        super();
        console.log("箭头高阶", '-->', 'C constructor');
    }
};

// cls = new C(Object); // 不可以new,因为是一个普通函数,它的返回值是一个带constructor的类
let cls = C(A); // 调用它返回一个类,一个带constructor的class
console.log(1, '-->', cls);
let c = new cls();
console.log(2, '-->', c);

// 其它写法
let c1 = new (C(Object))(); // new优先级太高了,所以后面要加括号才能先调用

可以改造上面序列化的例子了:

const Serialization = Sup => class extends Sup {
    constructor(...args) {
        console.log('Serialization constructor~~~~');
        super(...args);
        if (typeof (this.stringify) !== 'function') {
            throw new ReferenceError('should define stringify.');
        }
    }
}

class Point {
    constructor(x, y) {
        console.log('Point Constructor~~~~');
        this.x = x;
        this.y = y;
    }
}

class Point3D extends Serialization(Point) {
    constructor(x, y, z) {
        super(x, y); // super是Serialization(Point)包装过的新类型
        this.z = z;
    }
    stringify() {
        return `<Point3D ${this.x}.${this.y}.>`;
    }
}

let p3d = new Point3D(70, 80, 90);
console.log(p3d.stringify());

注意:

Serialization(Point) 这一步实际上是一个匿名箭头函数调用,返回了一个新的类型,Point3D 继承自这个新的匿名类型,增强了功能。

React 框架大量使用了这种 Mixin 技术。

5.3、异常

5.3.1、抛出异常

Js 的异常语法和 Java 相同,使用 throw 关键字抛出。使用 throw 关键字可以抛出任意对象的异常。

throw new Error('new error');
throw new ReferenceError('Ref Error');
throw 1;
throw 'not ok';
throw [1, 2, 3];
throw { 'a': 1 };
throw () => { }; // 函数

5.3.2、捕获异常

try...catch 语句捕获异常。

try...catch...finally 语句捕获异常,finally 保证最终一定执行。

注意这里的 catch 不支持类型,也就是说至多一个 catch 语句。可以在 catch 的语句块内,自行处理异常。

try {
    // throw new Error('new error');
    // throw new ReferenceError('Ref Error');
    // throw 1;
    // throw new Number(100);
    // throw 'not ok';
    // throw [1,2,3];
    // throw {'a':1};
    throw () => { }; // 函数
} catch (error) {
    console.log(typeof (error));
    console.log(error.constructor.name);
} finally {
    console.log('===end===')
}

5.4、ES6 模块化

ES6 之前,JS 没有出现模块化系统。JS 主要在前端的浏览器中使用,js 文件下载缓存到客户端,在浏览器中执行。比如简单的表单本地验证,漂浮一个广告。

服务器端使用 ASP、JSP 等动态网页技术,将动态生成数据嵌入一个 HTML 模板,里面夹杂着 JS 后使用 <script> 标签,返回浏览器端。这时候的 JS 只是一些简单函数和语句的组合。

2005 年之后,随着 Google 大量使用了 AJAX 技术之后,可以异步请求服务器端数据,带来了前端交互的巨大变化。

前端功能需求越来越多,代码也越来越多。随着 js 文件的增多,灾难性的后果产生了。由于习惯了随便写,js 脚本中各种全局变量污染,函数名冲突,无法表达脚本之间的依赖关系,因为都是用脚本文件先后加载来实现的。亟待模块化的出现。

2008 年 V8 引擎发布,2009 年诞生了 Nodejs,支持服务端 JS 编程,但没有模块化是不可以的。之后产生了 commonjs 规范。commonjs 规范,使用全局 require 函数导入模块,使用 exports 导出变量。

var math = require('math');

math.add(2,3); // 5

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得 CommonJS 规范不适用于浏览器环境。第二行 math.add(2, 3),在第一行 require('math') 之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是 AMD 规范诞生的背景。

AMD(Asynchronous Module Definition)采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

AMD 也采用 require() 语句加载模块,但是不同于 CommonJS,它要求两个参数:

require([module], callback);

第一个参数 [module],是一个数组,里面的成员就是要加载的模块;第二个参数 callback,则是模块加载成功之后的回调函数。如果将前面的代码改写成 AMD 形式,就是下面这样:

require(['math'], function (math) {
    math.add(2, 3);
});

math.add() 与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。

CMD(Common Module Definition),使用 seajs,作者是淘宝前端玉伯,兼容并解决了 RequireJs 的问题。

CMD 推崇 as lazy as possible,尽可能的懒加载。

由于社区的模块化呼声很高,ES6 开始提供支持模块的语法,但是浏览器目前支持还不够。

import 语句,导入另一个模块导出的绑定。

export 语句,从模块中导出函数、对象、值的,供其它模块 import 导入用。

5.4.1、导出

建立一个模块目录 src,然后在这个目录下新建一个 moda.js,内容如下:

// 缺省导出
export default class A {
    constructor(x) {
        this.x = x;
    }
    show() {
        console.log(this.x);
    }
}

// 导出函数
export function foo() {
    console.log('foo function');
}

// 导出常量
export const CONSTA = 'aaa';

5.4.2、导入

其它模块中导入语句如下:

import { A, foo } from "./src/moda";
import * as mod_a from "./src/moda";

let a = A()
foo()
console.log(mod_a.CONSTA)

/* 运行结果
Error: 
(node:4412) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)

Error: 
c:\Users\Brinnatt\Documents\javascript\node_85bb2e553b294.tmp.js:1
import { A, foo } from "./src/moda";
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1187:20)
    at Module._compile (node:internal/modules/cjs/loader:1231:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1321:10)
    at Module.load (node:internal/modules/cjs/loader:1125:32)
    at Module._load (node:internal/modules/cjs/loader:965:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12)
    at node:internal/main/run_main_module:23:47

Node.js v20.0.0
*/

VS Code 可以很好的语法支持了,但是运行环境,包括 V8 引擎,都不能很好的支持模块化语法。

5.5、转译工具

转译就是从一种语言代码转换到另一个语言代码,当然也可以从高版本转译到低版本的支持语句。

由于 JS 存在不同版本,不同浏览器兼容的问题,如何解决对语法的支持问题?

使用 transpiler 转译工具解决。

5.5.1、babel

开发中可以使用较新的 ES6 语法,通过转译器转换为指定的某些版本代码。

官网:http://babeljs.io/

打开 Try it out,测试一段代码:

function* counter() {
    let i = 0;
    while (true)
        yield (++i);
}
g = counter();
console.log(g.next().value);

5.5.2、预设

有如下一些预设 presets,我们先看看有哪些,一会儿再进行预设的安装和配置。

presets:
babel-preset-env 当前环境支持的代码,新target

# ES2015转码规则
$ npm install --save-dev babel-preset-es2015

# react转码规则
$ npm install --save-dev babel-preset-react

# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

5.5.3、离线转译安装配置

5.5.3.1、初始化 npm

在项目目录中使用:

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (javascript) test
version: (1.0.0)
description: babel
entry point: (index.js)
test command:
git repository:
keywords:
author: Brinnatt
license: (ISC)
About to write to C:\Users\Brinnatt\Documents\javascript\package.json:

{
  "name": "test",
  "version": "1.0.0",
  "description": "babel",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Brinnatt",
  "license": "ISC"
}

Is this OK? (yes) yes

在项目根目录下会生成 package.json 文件,内容就是上面花括号的内容。

5.5.3.2、设置镜像

.npmrc 文件,可以放到 npm 的目录下 npmrc 文件中,可以放到用户家目录中。

本次放到项目根目录中,内容如下:

registry=https://registry.npm.taobao.org

$ echo "registry=https://registry.npm.taobao.org" > .npmrc

5.5.3.3、安装

项目根目录下执行:

$ npm install babel-core babel-cli --save-dev

--save-dev说明
当你为你的模块安装一个依赖模块时,正常情况下你得先安装他们(在模块根目录下npm install module-name),然
后连同版本号手动将他们添加到模块配置文件package.json中的依赖里(dependencies)。开发用。
--save和--save-dev可以省掉你手动修改package.json文件的步骤。
spm install module-name --save 自动把模块和版本号添加到dependencies部分
spm install module-name --save-dev 自动把模块和版本号添加到devdependencies部分

安装完后,在项目根目录下出现 node_modules 目录 ,里面有 babel 相关模块及依赖的模块。

5.5.3.4、修改 package.json

替换为 scripts 的部分:

{
  "name": "test",
  "version": "1.0.0",
  "description": "babel",
  "main": "index.js",
  "scripts": {
    "build": "babel src -d lib"
  },
  "author": "Brinnatt",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.3"
  }
}

babel src -d lib 意思是从 src 目录中转译后的文件输出到 lib 目录。

5.5.3.5、准备目录

项目根目录下建立 src 和 lib 目录。src 是源码目录;lib 是目标目录。

5.5.3.6、配置 babel 和安装依赖

在项目根目录下创建 .babelrc 文件,Json 格式。

{
    "presets": ["env"]
}

env 可以根据当前环境自动选择。

安装依赖:

$ npm install babel-preset-env --save-dev

5.5.3.7、准备 js 文件

在 src 中的 moda.js

// 缺省导出
export default class A {
    constructor(x) {
        this.x = x;
    }
    show() {
        console.log(this.x);
    }
}

src 目录下新建 index.js

import A from "./moda"

let a = new A(100);
a.show();

直接在 VS Code 的环境下执行出错。估计很难有能够正常运行的环境。所以,要转译为 ES5 的代码。

在项目根目录下执行命令:

PS C:\Users\Brinnatt\Documents\javascript> npm run build
Debugger attached.

> test@1.0.0 build
> babel src -d lib

Debugger attached.
src\index.js -> lib\index.js
src\moda.js -> lib\moda.js
Waiting for the debugger to disconnect...
Waiting for the debugger to disconnect...

可以看到,2 个文件被转译,运行文件:

PS C:\Users\Brinnatt\Documents\javascript> node .\lib\index.js
Debugger attached.
100
Waiting for the debugger to disconnect...

使用 babel 等转译器转译 JS 非常流行。

开发者可以在高版本中使用新的语法特性,提高开发效率,把兼容性问题交给转译器处理。

5.5.4、导入导出

上面示例中,导出代码都在 src/moda.js 中,导入代码都写在 src/index.js 中,不再赘述。

5.5.4.1、缺省导入导出

只允许一个缺省导出,缺省导出可以是变量、函数、类,但不能使用 let、var、const 关键字作为默认导出。

// 缺省导出 匿名函数
export default function() {
    console.log('default export function')
}

// 缺省导入
import defaultFunc from './mod'
defaultFunc();
// 缺省导出 命名函数
export default function xyz() {
    console.log('default export function')
}

// 缺省导入
import defaultFunc from './mod'
defaultFunc();

缺省导入的时候,可以自己重新命名,不需要和缺省导出时一致。

缺省导入,不需要在 import 后使用花括号。

5.5.4.2、命名导入导出

moda.js 导出代码如下:

/**
* 导出举例
*/
// 缺省导出类
export default class {
    constructor(x) {
        this.x = x;
    }
    show() {
        console.log(this.x);
    }
}

// 命名导出 函数
export function foo() {
    console.log('regular foo()');
}

// 函数定义
function bar() {
    console.log('regular bar()');
}

// 变量常量定义
let x = 100;
var y = 200;
const z = 300;

// 导出
export { bar, x, y, z };

index.js 导入代码如下:

/**
* 导如举例
* as 设置别名
*/
import defaultCls, { foo, bar, x, y, z as CONST_C } from './moda';
foo();
bar();
console.log(x); // x 只读,不可修改,x++ 异常
console.log(y); // y 只读
console.log(CONST_C);
new defaultCls(1000).show();

也可以使用下面的形式,导入所有导出,但是会定义一个新的名词空间。使用名词空间可以避免冲突。

import * as newmod from './moda';

newmod.foo();
newmod.bar();
new newmod.default(2000).show();

5.6、解构

JS 的解构很灵活,参考:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator

5.6.1、列表解构

var parts = ['shoulder', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes']; // 使用...解构
console.log(lyrics) // [ 'head', 'shoulder', 'knees', 'and', 'toes' ]

5.6.2、参数解构

function f(x, y, z) {
    console.log(x + y + z)
}
var args = [2, 3, 4];
f(...args);

5.6.3、数组解构

JS 的数组解构非常强大。

const arr = [100, 200, 300];
let [x, y, z] = arr;
console.log(1, '-->', x, y, z);

// 丢弃
const [, b,] = arr;
console.log(2, '-->', b);

// b = 5 // 异常,b 声明为 const
// 少于数组元素
const [d, e] = arr;
console.log(3, '-->', d, e);

// 多于数组元素
const [m, n, o, p] = arr
console.log(4, '-->', m, n, o, p);

// 可变变量
const [f, ...args] = arr
console.log(5, '-->', f);
console.log(5, '-->', args);

// 支持默认值
const [j = 1, k, , , l = 10] = arr
console.log(6, '-->', j, k, l);

/* 输出结果
1 --> 100 200 300
2 --> 200
3 --> 100 200
4 --> 100 200 300 undefined
5 --> 100
5 --> [ 200, 300 ]
6 --> 100 200 10
*/

解构的时候,变量从左到右和元素对齐,可变参数放到最右边。

能对应到数据就返回数据,对应不到数据的返回默认值,如果没有默认值返回 undefined。

5.6.4、对象解构

简单对象解构:

const obj = {
    a: 100,
    b: 200,
    c: 300
};

let { x, y, z } = obj;
console.log(1, '-->', x, y, z); // undefined undefined undefined

let { a, b, c } = obj; // key名称
console.log(2, '-->', a, b, c); // 100 200 300

let { a: m, b: n, c: o } = obj; // 重命名
console.log(3, '-->', m, n, o);

let { a: M, c: N, d: D = 'python' } = obj; // 缺省值
console.log(4, '-->', M, N, D);

解构时,需要提供对象的属性名,会根据属性名找到对应的值。没有找到的返回缺省值,没有缺省值则返回 undefined。

5.6.5、复杂结构

嵌套数组:

const arr = [1, [2, 3], 4];
const [a, [b, c], d] = arr;
console.log(1, '-->', a, b, c, d); // 1 2 3 4

const [e, f] = arr;
console.log(2, '-->', e, f); //1 [ 2, 3 ]

const [g, h, i, j = 18] = arr;
console.log(3, '-->', g, h, i, j); //1 [ 2, 3 ] 4 18

const [k, ...l] = arr;
console.log(4, '-->', k, l); //1 [ [ 2, 3 ], 4 ]

对象:

var metadata = {
    title: "Scratchpad",
    translations: [
        {
            locale: "de",
            localization_tags: [],
            last_edit: "2014-04-14T08:43:37",
            url: "/de/docs/Tools/Scratchpad",
            title: "JavaScript-Umgebung"
        }
    ],
    url: "/en-US/docs/Tools/Scratchpad"
};

// 对应取出需要的参数
var { title: enTitle, translations: [{ title: localeTitle }] } = metadata;
console.log(enTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

5.6.6、数组的操作

方法 描述
push(...items) 尾部增加多个元素
pop() 移除最后一个元素,并返回它
map 引入处理函数来处理数组中每一个元素,返回新的数组
filter 引入处理函数处理数组中每一个元素,此处理函数返回true的元素保留,否则该元素被过滤掉,保留的元素构成新的数组返回
foreach 迭代所有元素,无返回值
const arr = [1, 2, 3, 4, 5];
arr.push(6, 7);
console.log(1, '-->', arr);

arr.pop()
console.log(2, '-->', arr);

// map
const powerArr = arr.map(x => x * x); // 新数组
console.log(3, '-->', powerArr);

const newarr = arr.forEach(x => x + 10); // 无返回值
console.log(4, '-->', newarr, arr);
console.log(5, '-->', arr.filter(x => x % 2 == 0)) // 新数组

5.6.7、数组练习

有一个数组 const arr = [1, 2, 3, 4, 5],要求算出所有元素平方值是偶数且大于 10 的。

const arr = [1, 2, 3, 4, 5]
// 这种实现好吗?
console.log(arr.map(x => x * x).filter(x => x % 2 === 0).filter(x => x > 10));

应该先过滤,再求值比较好。

const arr = [1, 2, 3, 4, 5]

// 1
console.log(arr.filter(x => x % 2 === 0).map(x => x * x).filter(x => x > 10)); // 先过滤减少迭代次数

// 2
s = Math.sqrt(10) // 10开方算一次
console.log(arr.filter(x => x > s && !(x % 2)).map(x => x * x))

// 3
let newarr = []
arr.forEach(x => {
    if (x > s && !(x % 2)) newarr.push(x * x);
})
console.log(newarr);

5.6.8、对象的操作

Object 的静态方法 描述
Object.keys(obj) ES5开始支持。返回所有key
Object.values(obj) 返回所有值,试验阶段,支持较差
Object.entries(obj) 返回所有值,试验阶段,支持较差
Object.assign(target, ...sources) 使用多个source对象,来填充target对象,返回target对象
const obj = {
    a: 100,
    b: 200,
    c: 300
};
console.log(Object.keys(obj)); // key,ES5
console.log(Object.values(obj)); // 值,实验性
console.log(Object.entries(obj)); // 键值对,实验性

// assign
var metadata = {
    title: "Scratchpad",
    translations: [
        {
            locale: "de",
            localization_tags: [],
            last_edit: "2014-04-14T08:43:37",
            url: "/de/docs/Tools/Scratchpad",
            title: "JavaScript-Umgebung"
        }
    ],
    url: "/en-US/docs/Tools/Scratchpad"
};
var copy = Object.assign({}/*目标对象*/, metadata,
    { schoolName: 'brinnatt', url: 'www.brinnatt.com' }/*增加新的属性,覆盖同名属性*/,
    { translations: null } /*覆盖metadata的translations*/
);
console.log(copy);

5.7、Promise

5.7.1、概念

Promise 对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。

简单说,就是处理异步请求的。

之所以叫做 Promise,就是我承诺,如果成功则怎么处理,失败则怎么处理。

// 语法
new Promise(
    /* executor */
    function (resolve, reject) {...}
);

5.7.1.1、executor

executor 是一个带有 resolve 和 reject 两个参数的函数 。

executor 函数在 Promise 构造函数执行时同步执行,被传递 resolve 和 reject 函数(executor 函数在 Promise 构造函数返回新建对象前被调用)。

executor 内部通常会执行一些异步操作,一旦完成,可以调用 resolve 函数来将 promise 状态改成 fulfilled 即完成,或者在发生错误时将它的状态改为 rejected 即失败。

如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。

executor 中,resolve 或 reject 只能执行其中一个函数。

5.7.1.2、Promise 的状态

pending:初始状态,不是成功或失败状态。

fulfilled:意味着操作成功完成。

rejected:意味着操作失败。

5.7.1.3、Promise.then(onFulfilled, onRejected)

参数是 2 个函数,根据 Promise 的状态来调用不同的函数,fulfilled 走 onFulfilled 函数,rejected 走 onRejected 函数。

then 的返回值是一个新的 promise 对象。调用任何一个参数后,其返回值会被新的 promise 对象来 resolve,向后传递。

// 简单使用
var myPromise = new Promise((resolve, reject) => {
    resolve('hello'); // 执行,置状态为 fulfilled
    console.log('(-> -------- This is line -------- <-)');
    reject('world'); // 永远执行不到
});
console.log(myPromise);

myPromise.then(
    /*如果成功则显示结果*/
    (value) => console.log(1, '-->', myPromise, value),
    /*如果失败则显示原因*/
    (reason) => console.log(2, '-->', myPromise, reason)
);

5.7.1.4、catch(onRejected)

为当前 Promise 对象添加一个拒绝回调,返回一个新的 Promise 对象。onRejected 函数调用其返回值会被新的 Promise 对象用来 resolve。

var myPromise = new Promise((resolve, reject) => {
    //resolve('hello'); // 执行,置状态为fulfilled
    console.log('(-> -------- This is line -------- <-)');
    reject('world'); // 可以执行了
});
console.log(myPromise);

// 链式处理
myPromise.then(
    /*如果成功则显示结果*/
    (value) => console.log(1, '-->', myPromise, value),
    /*如果失败则显示原因*/
    (reason) => console.log(2, '-->', myPromise, reason)
).then(
    function (v) {
        console.log(2.5, '-->', v);
        return Promise.reject(v + '***') //
    }
).catch(reason => {
    console.log(3, '-->', reason);
    return Promise.resolve(reason);
})

5.7.2、异步实例

function runAsync() {
    return new Promise(function (resolve, reject) {
        // 异步操作
        setTimeout(function () {
            console.log('do sth...');
            resolve('ok...');
        }, 3000);
    });
}
// 调用
runAsync().then(value => {
    console.log(value);
    return Promise.reject(value + '*');
}).catch(reason => {
    console.log(reason);
    return Promise.resolve(reason + '*');
}).then(value => {
    console.log(value);
    console.log('END');
});
console.log('~~~~~~ FIN ~~~~~~~')
标签云