ECMAScript6:模块化编程

modules

模块

The old way

传统JavaScript有相当多的模块化方案(AMD,CommonJS,UMD等)。大体都是通过一个外部函数返回一个携带各种闭包的对象,达到将公用的API暴露出来的目的。类似这样

function Hello(name) {
  function greeting() {
    console.log( "Hello " + name + "!" );
  }
  // public API
  return {
    greeting: greeting
  };
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!

之所以会诞生出这么多方案,无疑是需求跑在了前面,而语法不直接支持这些。于是乎ES6吸收了一些C系语言的思想,直接支持模块化。

Before the new way

在具体介绍语法之前,来看看ES6模块化和以往有何不同

  • ES6模块化是基于文件的,一个文件只能有一个模块;

ES6 modules are file-based, meaning one module per file. At this time, there is no standardized way of combining multiple modules into a single file.(ES6模块化是基于文件的,意味着每一个文件只有一个模块。与此同时,没有一个将多个模块合并在同一个文件内的标准化的方式)

  • ES6模块化是单例的;

Every time you import that module into another module, you get a reference to the one centralized instance. If you want to be able to produce multiple module instances, your module will need to provide some sort of factory to do it.(每次引用一个模块,实际上只是获取了一个中心实例的引用。如果你想生成多个模块实例,你的模块需要提供某种工厂模式来实现)

The new way

exports & import

一个模块包含多个独立的子功能,如果需要公开相应的功能模块,直接用export关键字即可,来看一个例子:

// exports.js
export function foo(canvas, options) {
  //可以导出function
}
export class bar {
  //可以导出class
}
export {foo,bar} //可以是Object

导入模块

import {foo, bar} from "kittydar.js";
//重命名
import {foo as fooRename} from "kittydar.js";
import {bar as barRename} from "kittydar.js";

export default

ES6完全支持import语句从历史项目AMD或CommonJS模块化方案中导入模块。

来看代码

//传统CommonJS写法
module.export = {
  field1: value1,
  field2: function(){
    //implements
  }
}
//ES6写法
//exportDefault.js
export default {
   field1: value1,
   field2: function(){
     //implements
   }
};

两者几乎等价,这意味着export输出模块本质上和CommonJS模块化方案没什么区别,ES6完全可以从历史代码引入模块,减少代码迁移的痛苦。

import expDef from 'exportDefault.js';
import $ from 'jQuery.js';
import _ from 'lodash'

循环依赖

A依赖B,B依赖A,怎么做到不出问题呢?
《You Don't Know JS ES6》书中给出了一个例子:

//Module A
import bar from "B";
export default function foo(x) {
  if(x>10)return bar(x-1);
  return x * 2;
}
//Module B
import foo from "A";
export default function bar(y) {
  if(y>5)return foo(y/2);
  return y * 3;
}

我们来试想一下,foobar两个函数在同一个函数作用域内,运行起来自然是没问题的,然而在模块化环境下,ES6则需要做一些额外的工作才能使得如此循环依赖生效。但是,怎么做到呢?

In essence, the mutual imports, along with the static verification that’s done to validate both import statements, virtually composes the two separate module scopes (via the bindings), such that foo(..) can call bar(..) and vice versa. This is symmetric to if they had originally been declared in the same scope.

大概意思是本质来说,实际上这样的循环依赖将两个模块(A和B)的函数作用域虚拟地联合在了一起,如此一来foo可以调用bar,bar也可以调用foo。这和将foo和bar两个函数声明在同一个函数作用域的实际效果是一样的。

模块对象 & 聚合模块

使用import *实际上导出的是模块命名空间对象,将所有模块的属性全部导出。

import * as cows from "cows";
cows.moon();

聚合模块又是什么呢?很好理解,将其他细小模块整理起来,在同一个模块里聚合,并导出。类似于先import...from了模块,然后再将其export,这样的操作无非就是把两步合成一步来做。

// 导入"sri-lanka"并将它导出的内容的一部分重新导出
export {Tea, Cinnamon} from "sri-lanka";
// 导入"equatorial-guinea"并将它导出的内容的一部分重新导出
export {Coffee, Cocoa} from "equatorial-guinea";
// 导入"singapore"并将它导出的内容全部导出
export * from "singapore";

Classes 类

墙裂声明:此类非彼类,JavaScript中的Classes和其他语言Classes虽然类似,但有本质的区别,而JS中所谓的Classes只不过是语法糖。

实现原理

At the heart of the new ES6 class mechanism is the class keyword, which identifies a block where the contents define the members of a function’s prototype.(class机制核心在于class关键字,这标识了一个定义了函数原型上面的成员的语句块)

有点拗口,直接来个例子:

class Foo {
  constructor(a,b) {
    this.x = a;
    this.y = b;
  }
  gimmeXY() {
    return this.x * this.y;
  }
}
//等价于
function Foo(a,b) {
  this.x = a;
  this.y = b;
}
Foo.prototype.gimmeXY = function() {
  return this.x * this.y;
}

可以这么说,你所看到的class的内容,实际上就是该函数的原型对象(Prototype Object)本身。

我们趁机来复习一下JavaScript的Prototype和Constructor吧。

每一个函数都包含一个prototype属性,这个属性是指向一个对象的引用,这个对象称之为“原型对象”(prototype object)。每一个函数包含不同的原型对象。当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。

在JavaScript中,类的实现是基于其原型继承机制的。如果两个实例都从同一个原型对象上继承了属性,我们说他们是同一个类的实例。

如果两个对象继承自同一个原型,往往意味着(但不是绝对)他们是由同一个构造函数创建并初始化的

构造函数是用来初始化新创建的对象的。使用new调用构造函数会自动创建一个新对象,因此构造函数本身只需初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototye属性被用作新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此他们是同一个类的成员。

---引自《大犀牛》

认真看看下面的Demo

function Foo(a,b) {
        this.x = a;
    this.y = b;
    console.log('Foo is constructor');
    return 'ok';
}

function Bar(a,b) {
        this.x = a;
    this.y = b;
    console.log('Bar is constructor');
    return 'ok';
}

Foo.prototype.getXYmutiply = function() {
    return this.x * this.y;
}
//通过原型对象传递实现方法和属性的继承
Bar.prototype = Foo.prototype;//原型对象传递(继承)
var bar = new Bar(5,4);//Bar is constructor
var foo = new Foo(5,6);//Foo is constructor
/** 此时 bar 和 foo 是同一个原型对象 **/
console.log(Bar.prototype === Foo.prototype);//true
console.log(Bar.prototype.constructor);//function Foo(){[....]}
Bar.prototype.constructor = Bar;
bar = new Bar(5,4);//Bar is constructor
console.log(Bar.prototype.constructor);//function Bar(){[....]}
console.log(bar.getXYmutiply());//20

所以我们很清楚,ES6所谓的Classes特性只是在ES5基础上做了美化,用贴近其他语言语法(Java)的形式,模拟出其他语言有的面向对象的特性(继承,多态,封装),使得代码看起来更加明确易懂。

extends & super

接着上面的Foo Bar例子

class Foo {
  constructor(a,b) {
    this.x = a;
    this.y = b;
  }
  gimmeXY() {
    return this.x * this.y;
  }
}
class Bar extends Foo {
  constructor(a,b,c) {
    super( a, b );
    this.z = c;
  }
  gimmeXYZ() {
    return super.gimmeXY() * this.z;
  }
}
var b = new Bar(5,15,25);

b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXY();//75
b.gimmeXYZ(); // 1875

仔细想想,在pre-ES6的时代,要模拟面向对象是一件很蛋疼的事情。首先我们熟知面向对象三个重要特征,继承、多态、封装。尤其是继承,传统的做法都是通过父类将原型对象赋值给子类原型对象实现的,与此同时还要注意,在原型对象赋值以后,需要更正子类原型对象中的构造方法,blabla...特别啰嗦。ES6这次索性把以上我们说的一整套流程固定成了几个语法糖,在明确语义的同时,减少了重复啰嗦的代码实现。
再来看一组代码对照,一切都会很清晰了。

function Foo() {
  this.a = 1;
}
function Bar() {
  this.b = 2;
  Foo.call( this );
}
// `Bar` "extends" `Foo`
Bar.prototype = Object.create( Foo.prototype );


//等价于
class Foo {
  constructor() {
    this.a = 1;
  }
}
class Bar extends Foo {
  constructor() {
    this.b = 2;
    super();
  }
}

new.target & static

老规矩,先看代码

class Foo {
  static answer = 42;
  static cool() {
    console.log( "cool" );
  }
  // ..
}
class Bar extends Foo {
  constructor() {
    console.log( new.target.answer );
  }
}
Foo.answer;// 42
Bar.answer;// 42
var b = new Bar();// 42
b.cool();// "cool"
b.answer;// undefined -- `answer` is static on `Foo`

Be careful not to get confused that static members are on the class’s prototype chain. They’re actually on the dual/parallel chain between the function constructors.(静态成员并不在类的原型链里,而是存在于两个函数间的构造方法内)

如上所说,离开了构造方法,使用new.target是无效的。

模块化编程部分暂时介绍到这里,如果遇到什么没介绍到的,我会及时补充进来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容