天天看點

JavaScript的子產品講解

介紹

子產品通常是指程式設計語言所提供的代碼組織機制,利用此機制可将程式拆解為獨立且通用的代碼單元。所謂子產品化主要是解決代碼分割、作用域隔離、子產品之間的依賴管理以及釋出到生産環境時的自動化打包與處理等多個方面。

子產品的優點

1.可維護性。 因為子產品是獨立的,一個設計良好的子產品會讓外面的代碼對自己的依賴越少越好,這樣自己就可以獨立去更新和改進。

2.命名空間。 在 JavaScript 裡面,如果一個變量在最頂級的函數之外聲明,它就直接變成全局可用。是以,常常不小心出現命名沖突的情況。使用子產品化開發來封裝變量,可以避免污染全局環境。

3.重用代碼。 我們有時候會喜歡從之前寫過的項目中拷貝代碼到新的項目,這沒有問題,但是更好的方法是,通過子產品引用的方式,來避免重複的代碼庫。

CommonJS

CommonJS 最開始是 Mozilla 的工程師于 2009 年開始的一個項目,它的目的是讓浏覽器之外的 JavaScript (比如伺服器端或者桌面端)能夠通過子產品化的方式來開發和協作。

在 CommonJS 的規範中,每個 JavaScript 檔案就是一個獨立的子產品上下文(module context),在這個上下文中預設建立的屬性都是私有的。也就是說,在一個檔案定義的變量(還包括函數和類),都是私有的,對其他檔案是不可見的。

需要注意的是,CommonJS 規範的主要适用場景是伺服器端程式設計,是以采用同步加載子產品的政策。如果我們依賴3個子產品,代碼會一個一個依次加載它們。

該子產品實作方案主要包含 require 與 module 這兩個關鍵字,其允許某個子產品對外暴露部分接口并且由其他子產品導入使用。

//sayModule.js
function SayModule () {
    this.hello = function () {
        console.log('hello');
    };

    this.goodbye = function () {
        console.log('goodbye');
    };
}

module.exports = SayModule;

//main.js 引入sayModule.js
var Say = require('./sayModule.js');
var sayer = new Say();
sayer.hello(); //hello           

作為一個伺服器端的解決方案,CommonJS 需要一個相容的腳本加載器作為前提條件。該腳本加載器必須支援名為 require 和 module.exports 的函數,它們将子產品互相導入導出。

Node.js

Node 從 CommonJS 的一些創意中,創造出自己的子產品化實作。由于Node 在服務端的流行,Node 的子產品形式被(不正确地)稱為 CommonJS。

Node.js子產品可以分為兩大類,一類是核心子產品,另一類是檔案子產品。

核心子產品 就是Node.js标準的API中提供的子產品,如fs、http、net等,這些都是由Node.js官方提供的子產品,編譯成了二進制代碼,可以直接通過require擷取核心子產品,例如require(‘fs’),核心子產品擁有最高的加載優先級,如果有子產品與核心子產品命名沖突,Node.js總是會加載核心子產品。

檔案子產品 是存儲為單獨的檔案(或檔案夾)的子產品,可能是JavaScript代碼、JSON或編譯好的C/C++代碼。在不顯式指定檔案子產品擴充名的時候,Node.js會分别試圖加上.js、.json、.node(編譯好的C/C++代碼)。

加載方式
  • 按路徑加載子產品

如果require參數一”/”開頭,那麼就以絕對路徑的方式查找子產品名稱,如果參數一”./”、”../”開頭,那麼則是以相對路徑的方式來查找子產品。

  • 通過查找node_modules目錄加載子產品

如果require參數不以”/”、”./”、”../”開頭,而該子產品又不是核心子產品,那麼就要通過查找node_modules加載子產品了。我們使用的npm擷取的包通常就是以這種方式加載的。

加載緩存

Node.js子產品不會被重複加載,這是因為Node.js通過檔案名緩存所有加載過的檔案子產品,是以以後再通路到時就不會重新加載了。

注意: Node.js是根據實際檔案名緩存的,而不是require()提供的參數緩存的,也就是說即使你分别通過require(‘express’)和require(‘./node_modules/express’)加載兩次,也不會重複加載,因為盡管兩次參數不同,解析到的檔案卻是同一個。

Node.js 中的子產品在加載之後是以單例化運作,并且遵循值傳遞原則:如果是一個對象,就相當于這個對象的引用。

子產品載入過程

加載檔案子產品的工作,主要由原生子產品module來實作和完成,該原生子產品在啟動時已經被加載,程序直接調用到runMain靜态方法。

例如運作: node app.js

Module.runMain = function () {
    // Load the main module--the command line argument.
    Module._load(process.argv[1], null, true);
};

//_load靜态方法在分析檔案名之後執行
var module = new Module(id, parent);

//并根據檔案路徑緩存目前子產品對象,該子產品執行個體對象則根據檔案名加載。
module.load(filename);           

具體說一下上文提到了檔案子產品的三類子產品,這三類檔案子產品以字尾來區分,Node.js會根據字尾名來決定加載方法,具體的加載方法在下文require.extensions中會介紹。

  • .js

    通過fs子產品同步讀取js檔案并編譯執行。
  • .node

    通過C/C++進行編寫的Addon。通過dlopen方法進行加載。
  • .json

    讀取檔案,調用JSON.parse解析加載。

接下來較長的描述js字尾的編譯過程。Node.js在編譯js檔案的過程中實際完成的步驟有對js檔案内容進行頭尾包裝。以app.js為例,包裝之後的app.js将會變成以下形式:

//circle.js
var PI = Math.PI;
exports.area = function (r) {
    return PI * r * r;
};
exports.circumference = function (r) {
    return 2 * PI * r;
};

//app.js
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));

//app包裝後
(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

//這段代碼會通過vm原生子產品的runInThisContext方法執行(類似eval,隻是具有明确上下文,不污染全局),傳回為一個具體的function對象。最後傳入module對象的exports,require方法,module,檔案名,目錄名作為實參并執行。           

這就是為什麼require并沒有定義在app.js 檔案中,但是這個方法卻存在的原因。從Node.js的API文檔中可以看到還有__filename、__dirname、module、exports幾個沒有定義但是卻存在的變量。其中__filename和__dirname在查找檔案路徑的過程中分析得到後傳入的。module變量是這個子產品對象自身,exports是在module的構造函數中初始化的一個空對象({},而不是null)。

在這個主檔案中,可以通過require方法去引入其餘的子產品。而其實這個require方法實際調用的就是module._load方法。

load方法在載入、編譯、緩存了module後,傳回module的exports對象。這就是circle.js檔案中隻有定義在exports對象上的方法才能被外部調用的原因。

以上所描述的子產品載入機制均定義在lib/module.js中。

require 函數

require 引入的對象主要是函數。當 Node 調用 require() 函數,并且傳遞一個檔案路徑給它的時候,Node 會經曆如下幾個步驟:

  • Resolving:找到檔案的絕對路徑;
  • Loading:判斷檔案内容類型;
  • Wrapping:打包,給這個檔案賦予一個私有作用範圍。這是使 require 和 module 子產品在本地引用的一種方法;
  • Evaluating:VM 對加載的代碼進行處理的地方;
  • Caching:當再次需要用這個檔案的時候,不需要重複一遍上面步驟。
require.extensions 來檢視對三種檔案的支援情況
JavaScript的子產品講解

可以清晰地看到 Node 對每種擴充名所使用的函數及其操作:對 .js 檔案使用 module._compile;對 .json 檔案使用 JSON.parse;對 .node 檔案使用 process.dlopen。

檔案查找政策

  • 從檔案子產品緩存中加載

盡管原生子產品與檔案子產品的優先級不同,但是優先級最高的是從檔案子產品的緩存中加載已經存在的子產品。

  • 從原生子產品加載

原生子產品的優先級僅次于檔案子產品緩存的優先級。require方法在解析檔案名之後,優先檢查子產品是否在原生子產品清單中。以http子產品為例,盡管在目錄下存在一個http、http.js、http.node、http.json檔案,require(“http”)都不會從這些檔案中加載,而是從原生子產品中加載。

原生子產品也有一個緩存區,同樣也是優先從緩存區加載。如果緩存區沒有被加載過,則調用原生子產品的加載方式進行加載和執行。

  • 從檔案加載

當檔案子產品緩存中不存在,而且不是原生子產品的時候,Node.js會解析require方法傳入的參數,并從檔案系統中加載實際的檔案,加載過程中的包裝和編譯細節在前面說過是調用load方法。

當 Node 遇到 require(X) 時,按下面的順序處理。

(1)如果 X 是内置子產品(比如 require('http')) 
  a. 傳回該子產品。 
  b. 不再繼續執行。

(2)如果 X 以 "./" 或者 "/" 或者 "../" 開頭 
  a. 根據 X 所在的父子產品,确定 X 的絕對路徑。 
  b. 将 X 當成檔案,依次查找下面檔案,隻要其中有一個存在,就傳回該檔案,不再繼續執行。
        X
        X.js
        X.json
        X.node

  c. 将 X 當成目錄,依次查找下面檔案,隻要其中有一個存在,就傳回該檔案,不再繼續執行。
        X/package.json(main字段)
        X/index.js
        X/index.json
        X/index.node

(3)如果 X 不帶路徑 
  a. 根據 X 所在的父子產品,确定 X 可能的安裝目錄。 
  b. 依次在每個目錄中,将 X 當成檔案名或目錄名加載。

(4) 抛出 "not found"           
JavaScript的子產品講解
子產品循環依賴
//建立兩個檔案,module1.js 和 module2.js,并且讓它們互相引用
    // module1.js
    exports.a = 1;
    require('./module2');
    exports.b = 2;
    exports.c = 3;

    // module2.js
    const Module1 = require('./module1');
    console.log('Module1 is partially loaded here', Module1);           

在 module1 完全加載之前需要先加載 module2,而 module2 的加載又需要 module1。這種狀态下,我們從 exports 對象中能得到的就是在發生循環依賴之前的這部分。上面代碼中,隻有 a 屬性被引入,因為 b 和 c 都需要在引入 module2 之後才能加載進來。

Node 使這個問題簡單化,在一個子產品加載期間開始建立 exports 對象。如果它需要引入其他子產品,并且有循環依賴,那麼隻能部分引入,也就是隻能引入發生循環依賴之前所定義的這部分。

AMD

AMD 是 Asynchronous Module Definition 的簡稱,即“異步子產品定義”,是從 CommonJS 讨論中誕生的。AMD 優先照顧浏覽器的子產品加載場景,使用了異步加載和回調的方式。

AMD 和 CommonJS 一樣需要腳本加載器,盡管 AMD 隻需要對 define 方法的支援。define 方法需要三個參數:子產品名稱,子產品運作的依賴數組,所有依賴都可用之後執行的函數(該函數按照依賴聲明的順序,接收依賴作為參數)。隻有函數參數是必須的。define 既是一種引用子產品的方式,也是定義子產品的方式。

// file lib/sayModule.js
define(function (){
    return {
        sayHello: function () {
            console.log('hello');
        }
    };
});

//file main.js
define(['./lib/sayModule'], function (say){
    say.sayHello(); //hello
})           

main.js 作為整個應用的入口子產品,我們使用 define 關鍵字聲明了該子產品以及外部依賴(沒有生命子產品名稱);當我們執行該子產品代碼時,也就是執行 define 函數的第二個參數中定義的函數功能,其會在架構将所有的其他依賴子產品加載完畢後被執行。這種延遲代碼執行的技術也就保證了依賴的并發加載。

RequireJS

RequireJS 是一個前端的子產品化管理的工具庫,遵循AMD規範,通過一個函數來将所有所需要的或者說所依賴的子產品實作裝載進來,然後傳回一個新的函數(子產品),我們所有的關于新子產品的業務代碼都在這個函數内部操作,其内部也可無限制的使用已經加載進來的以來的子產品。

<script data-main='scripts/main' src='scripts/require.js'></script>
//scripts下的main.js則是指定的主代碼腳本檔案,所有的依賴子產品代碼檔案都将從該檔案開始異步加載進入執行。           

defined用于定義子產品,RequireJS要求每個子產品均放在獨立的檔案之中。按照是否有依賴其他子產品的情況分為獨立子產品和非獨立子產品。

1、獨立子產品 不依賴其他子產品。直接定義

define({
    methodOne: function (){},
    methodTwo: function (){}
});

//等價于

define(function (){
    return {
        methodOne: function (){},
        methodTwo: function (){}
    };
});           

2、非獨立子產品,對其他子產品有依賴

define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){
    ...
});

//或者

define( function( require ){
    var mOne = require( 'moduleOne' ),
        mTwo = require( 'moduleTwo' );
    ...
});           

如上代碼, define中有依賴子產品數組的 和 沒有依賴子產品數組用require加載 這兩種定義子產品,調用子產品的方法合稱為AMD模式,定義子產品清晰,不會污染全局變量,清楚的顯示依賴關系。AMD模式可以用于浏覽器環境并且允許非同步加載子產品,也可以按需動态加載子產品。

CMD

CMD(Common Module Definition),在CMD中,一個子產品就是一個檔案。

全局函數define,用來定義子產品。

參數 factory 可以是一個函數,也可以為對象或者字元串。

當 factory 為對象、字元串時,表示子產品的接口就是該對象、字元串。

定義JSON資料子產品:

define({ "foo": "bar" });           
factory 為函數的時候,表示子產品的構造方法,執行構造方法便可以得到子產品向外提供的接口。

define( function(require, exports, module) { 
    // 子產品代碼
});           

SeaJS

sea.js 核心特征:

1.遵循CMD規範,與NodeJS般的書寫子產品代碼。

2.依賴自動加載,配置清晰簡潔。

seajs.use用來在頁面中加載一個或者多個子產品

// 加載一個子產品 
seajs.use('./a');

// 加載子產品,加載完成時執行回調
seajs.use('./a',function(a){
    a.doSomething();
});

// 加載多個子產品執行回調
seajs.use(['./a','./b'],function(a , b){
    a.doSomething();
    b.doSomething();
});           

AMD和CMD最大的差別是對依賴子產品的執行時機處理不同,注意不是加載的時機或者方式不同。

很多人說requireJS是異步加載子產品,SeaJS是同步加載子產品,這麼了解實際上是不準确的,其實加載子產品都是異步的,隻不過AMD依賴前置,js可以友善知道依賴子產品是誰,立即加載,而CMD就近依賴,需要使用把子產品變為字元串解析一遍才知道依賴了那些子產品,這也是很多人诟病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析子產品用的時間短到可以忽略。

為什麼說是執行時機處理不同?

同樣都是異步加載子產品,AMD在加載子產品完成後就會執行該子產品,所有子產品都加載執行完後會進入回調函數,執行主邏輯,這樣的效果就是依賴子產品的執行順序和書寫順序不一定一緻,看網絡速度,哪個先下載下傳下來,哪個先執行,但是主邏輯一定在所有依賴加載完成後才執行。

CMD加載完某個依賴子產品後并不執行,隻是下載下傳而已,在所有依賴子產品加載完成後進入主邏輯,遇到require語句的時候才執行對應的子產品,這樣子產品的執行順序和書寫順序是完全一緻的。

UMD

統一子產品定義(UMD:Universal Module Definition )就是将 AMD 和 CommonJS 合在一起的一種嘗試,常見的做法是将CommonJS 文法包裹在相容 AMD 的代碼中。

(function(define) {
    define(function () {
        return {
            sayHello: function () {
                console.log('hello');
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));           

該模式的核心思想在于所謂的 IIFE(Immediately Invoked Function Expression),該函數會根據環境來判斷需要的參數類别

ES6子產品(module)

嚴格模式

ES6 的子產品自動采用嚴格模式,不管有沒有在子產品頭部加上”use strict”;。

嚴格模式主要有以下限制。

  • 變量必須聲明後再使用
  • 函數的參數不能有同名屬性,否則報錯
  • 不能使用with語句
  • 不能對隻讀屬性指派,否則報錯
  • 不能使用字首0表示八進制數,否則報錯
  • 不能删除不可删除的屬性,否則報錯
  • 不能删除變量delete prop,會報錯,隻能删除屬性delete global[prop]
  • eval不會在它的外層作用域引入變量
  • eval和arguments不能被重新指派
  • arguments不會自動反映函數參數的變化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局對象
  • 不能使用fn.caller和fn.arguments擷取函數調用的堆棧
  • 增加了保留字(比如protected、static和interface)

子產品Module

一個子產品,就是一個對其他子產品暴露自己的屬性或者方法的檔案。

導出Export

作為一個子產品,它可以選擇性地給其他子產品暴露(提供)自己的屬性和方法,供其他子產品使用。

// profile.js
export var firstName = 'qiqi';
export var lastName = 'haobenben';
export var year = 1992;

//等價于

var firstName = 'qiqi';
var lastName = 'haobenben';
var year = 1992;
export {firstName, lastName, year}           

1、 通常情況下,export輸出的變量就是本來的名字,但是可以使用as關鍵字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

//上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2可以用不同的名字輸出兩次
。           

2、 需要特别注意的是,export指令規定的是對外的接口,必須與子產品内部的變量建立一一對應關系。

// 報錯
export 1;

// 報錯
var m = 1;
export m;

//上面兩種寫法都會報錯,因為沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法通過變量m,還是直接輸出1。1隻是一個值,不是接口。

/ 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};

//上面三種寫法都是正确的,規定了對外的接口m。其他腳本可以通過這個接口,取到值1。它們的實質是,在接口名與子產品内部變量之間,建立了一一對應的關系。           

3、最後,export指令可以出現在子產品的任何位置,隻要處于子產品頂層就可以。如果處于塊級作用域内,就會報錯,接下來說的import指令也是如此。

function foo() {
  export default 'bar' // SyntaxError
}
foo()           

導入import

作為一個子產品,可以根據需要,引入其他子產品的提供的屬性或者方法,供自己子產品使用。

1、 import指令接受一對大括号,裡面指定要從其他子產品導入的變量名。大括号裡面的變量名,必須與被導入子產品(profile.js)對外接口的名稱相同。如果想為輸入的變量重新取一個名字,import指令要使用as關鍵字,将輸入的變量重命名。

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

2、import後面的from指定子產品檔案的位置,可以是相對路徑,也可以是絕對路徑,.js路徑可以省略。如果隻是子產品名,不帶有路徑,那麼必須有配置檔案,告訴 JavaScript 引擎該子產品的位置。

3、注意,import指令具有提升效果,會提升到整個子產品的頭部,首先執行。

foo();

import { foo } from 'my_module';

//上面的代碼不會報錯,因為import的執行早于foo的調用。這種行為的本質是,import指令是編譯階段執行的,在代碼運作之前。           

4、由于import是靜态執行,是以不能使用表達式和變量,這些隻有在運作時才能得到結果的文法結構。

// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}           

5、最後,import語句會執行所加載的子產品,是以可以有下面的寫法。

import 'lodash';
//上面代碼僅僅執行lodash子產品,但是不輸入任何值。           

預設導出(export default)

每個子產品支援我們導出一個沒有名字的變量,使用關鍵語句export default來實作.

export default function(){
            console.log("I am default Fn");
        }
//使用export default關鍵字對外導出一個匿名函數,導入這個子產品的時候,可以為這個匿名函數取任意的名字

//取任意名字均可
import sayDefault from "./module-B.js";
sayDefault();
//結果:I am default Fn           

1、預設輸出和正常輸出的比較

// 第一組
export default function diff() { // 輸出
  // ...
}

import diff from 'diff'; // 輸入

// 第二組
export function diff() { // 輸出
  // ...
};

import {diff} from 'diff'; // 輸入

//上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括号;第二組是不使用export default時,對應的import語句需要使用大括号。           
export default指令用于指定子產品的預設輸出。顯然,一個子產品隻能有一個預設輸出,是以export default指令隻能使用一次。是以,import指令後面才不用加大括号,因為隻可能對應一個方法。

2、因為export default本質是将該指令後面的值,賦給default變量以後再預設,是以直接将一個值寫在export default之後。

// 正确
export default 42;

// 報錯
export 42;

//上面代碼中,後一句報錯是因為沒有指定對外的接口,而前一句指定外對接口為default。           

3、如果想在一條import語句中,同時輸入預設方法和其他變量,可以寫成下面這樣。

import _, { each } from 'lodash';

//對應上面代碼的export語句如下
export default function (){
    //...
}
export function each (obj, iterator, context){
    //...
}           

export 與 import 的複合寫法

如果在一個子產品之中,先輸入後輸出同一個子產品,import語句可以與export語句寫在一起。

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

/ 接口改名
export { foo as myFoo } from 'my_module';

// 整體輸出
export * from 'my_module';           

注意事項

1、聲明的變量,對外都是隻讀的。但是導出的是對象類型的值,就可修改。

2、導入不存在的變量,值為undefined。

ES6 中的循環引用

ES6 中,imports 是 exprts 的隻讀視圖,直白一點就是,imports 都指向 exports 原本的資料,比如:

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError           

是以在 ES6 中處理循環引用特别簡單,看下面這段代碼:

//------ a.js ------
import {bar} from 'b'; // (1)
export function foo() {
  bar(); // (2)
}

//------ b.js ------
import {foo} from 'a'; // (3)
export function bar() {
  if (Math.random()) {
    foo(); // (4)
  }
}           

假設先加載子產品 a,在子產品 a 加載完成之後,bar 間接性地指向的是子產品 b 中的 bar。無論是加載完成的 imports 還是未完成的 imports,imports 和 exports 之間都有一個間接的聯系,是以總是可以正常工作。

執行個體

//---module-B.js檔案---
//導出變量:name
export var name = "cfangxu";

moduleA子產品代碼:
//導入 子產品B的屬性 name    
import { name } from "./module-B.js";   
console.log(name)
//列印結果:cfangxu           

批量導出

//屬性name
var name = "cfangxu";
//屬性age
var age  = 26;
//方法 say
var say = function(){
            console.log("say hello");
         }
//批量導出
export {name,age,say}           

批量導入

//導入 子產品B的屬性
import { name,age,say } from "./module-B.js";
console.log(name)
//列印結果:cfangxu
console.log(age)
//列印結果:26
say()
//列印結果:say hello           

重命名導入變量

import {name as myName} from './module-B.js';
console.log(myName) //cfangxu
整體導入

/使用*實作整體導入
import * as obj from "./module-B.js";

console.log(obj.name)
//結果:"cfangxu"
console.log(obj.age)
//結果:26
obj.say();
//結果:say hello           

推薦資料

https://segmentfault.com/a/1190000012464333

//sayModule.js
function SayModule () {
    this.hello = function () {
        console.log('hello');
    };

    this.goodbye = function () {
        console.log('goodbye');
    };
}

module.exports = SayModule;

//main.js 引入sayModule.js
var Say = require('./sayModule.js');
var sayer = new Say();
sayer.hello(); //hello           
例如運作: node app.js

Module.runMain = function () {
    // Load the main module--the command line argument.
    Module._load(process.argv[1], null, true);
};

//_load靜态方法在分析檔案名之後執行
var module = new Module(id, parent);

//并根據檔案路徑緩存目前子產品對象,該子產品執行個體對象則根據檔案名加載。
module.load(filename);           
  • .js

  • .node

  • .json

//circle.js
var PI = Math.PI;
exports.area = function (r) {
    return PI * r * r;
};
exports.circumference = function (r) {
    return 2 * PI * r;
};

//app.js
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));

//app包裝後
(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

//這段代碼會通過vm原生子產品的runInThisContext方法執行(類似eval,隻是具有明确上下文,不污染全局),傳回為一個具體的function對象。最後傳入module對象的exports,require方法,module,檔案名,目錄名作為實參并執行。           
當 Node 遇到 require(X) 時,按下面的順序處理。

(1)如果 X 是内置子產品(比如 require('http')) 
  a. 傳回該子產品。 
  b. 不再繼續執行。

(2)如果 X 以 "./" 或者 "/" 或者 "../" 開頭 
  a. 根據 X 所在的父子產品,确定 X 的絕對路徑。 
  b. 将 X 當成檔案,依次查找下面檔案,隻要其中有一個存在,就傳回該檔案,不再繼續執行。
        X
        X.js
        X.json
        X.node

  c. 将 X 當成目錄,依次查找下面檔案,隻要其中有一個存在,就傳回該檔案,不再繼續執行。
        X/package.json(main字段)
        X/index.js
        X/index.json
        X/index.node

(3)如果 X 不帶路徑 
  a. 根據 X 所在的父子產品,确定 X 可能的安裝目錄。 
  b. 依次在每個目錄中,将 X 當成檔案名或目錄名加載。

(4) 抛出 "not found"           
//建立兩個檔案,module1.js 和 module2.js,并且讓它們互相引用
    // module1.js
    exports.a = 1;
    require('./module2');
    exports.b = 2;
    exports.c = 3;

    // module2.js
    const Module1 = require('./module1');
    console.log('Module1 is partially loaded here', Module1);           

// file lib/sayModule.js
define(function (){
    return {
        sayHello: function () {
            console.log('hello');
        }
    };
});

//file main.js
define(['./lib/sayModule'], function (say){
    say.sayHello(); //hello
})           
<script data-main='scripts/main' src='scripts/require.js'></script>
//scripts下的main.js則是指定的主代碼腳本檔案,所有的依賴子產品代碼檔案都将從該檔案開始異步加載進入執行。           
define({
    methodOne: function (){},
    methodTwo: function (){}
});

//等價于

define(function (){
    return {
        methodOne: function (){},
        methodTwo: function (){}
    };
});           
define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){
    ...
});

//或者

define( function( require ){
    var mOne = require( 'moduleOne' ),
        mTwo = require( 'moduleTwo' );
    ...
});           

define({ "foo": "bar" });           
factory 為函數的時候,表示子產品的構造方法,執行構造方法便可以得到子產品向外提供的接口。

define( function(require, exports, module) { 
    // 子產品代碼
});           
// 加載一個子產品 
seajs.use('./a');

// 加載子產品,加載完成時執行回調
seajs.use('./a',function(a){
    a.doSomething();
});

// 加載多個子產品執行回調
seajs.use(['./a','./b'],function(a , b){
    a.doSomething();
    b.doSomething();
});           

(function(define) {
    define(function () {
        return {
            sayHello: function () {
                console.log('hello');
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));           

// profile.js
export var firstName = 'qiqi';
export var lastName = 'haobenben';
export var year = 1992;

//等價于

var firstName = 'qiqi';
var lastName = 'haobenben';
var year = 1992;
export {firstName, lastName, year}           
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

//上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2可以用不同的名字輸出兩次
。           
// 報錯
export 1;

// 報錯
var m = 1;
export m;

//上面兩種寫法都會報錯,因為沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法通過變量m,還是直接輸出1。1隻是一個值,不是接口。

/ 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};

//上面三種寫法都是正确的,規定了對外的接口m。其他腳本可以通過這個接口,取到值1。它們的實質是,在接口名與子產品内部變量之間,建立了一一對應的關系。           
function foo() {
  export default 'bar' // SyntaxError
}
foo()           
import { lastName as surename } from './profile';           
foo();

import { foo } from 'my_module';

//上面的代碼不會報錯,因為import的執行早于foo的調用。這種行為的本質是,import指令是編譯階段執行的,在代碼運作之前。           
// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}           
import 'lodash';
//上面代碼僅僅執行lodash子產品,但是不輸入任何值。           

export default function(){
            console.log("I am default Fn");
        }
//使用export default關鍵字對外導出一個匿名函數,導入這個子產品的時候,可以為這個匿名函數取任意的名字

//取任意名字均可
import sayDefault from "./module-B.js";
sayDefault();
//結果:I am default Fn           
// 第一組
export default function diff() { // 輸出
  // ...
}

import diff from 'diff'; // 輸入

// 第二組
export function diff() { // 輸出
  // ...
};

import {diff} from 'diff'; // 輸入

//上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不需要使用大括号;第二組是不使用export default時,對應的import語句需要使用大括号。           
// 正确
export default 42;

// 報錯
export 42;

//上面代碼中,後一句報錯是因為沒有指定對外的接口,而前一句指定外對接口為default。           
import _, { each } from 'lodash';

//對應上面代碼的export語句如下
export default function (){
    //...
}
export function each (obj, iterator, context){
    //...
}           

export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

/ 接口改名
export { foo as myFoo } from 'my_module';

// 整體輸出
export * from 'my_module';           
//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError           
//------ a.js ------
import {bar} from 'b'; // (1)
export function foo() {
  bar(); // (2)
}

//------ b.js ------
import {foo} from 'a'; // (3)
export function bar() {
  if (Math.random()) {
    foo(); // (4)
  }
}           
//---module-B.js檔案---
//導出變量:name
export var name = "cfangxu";

moduleA子產品代碼:
//導入 子產品B的屬性 name    
import { name } from "./module-B.js";   
console.log(name)
//列印結果:cfangxu           
//屬性name
var name = "cfangxu";
//屬性age
var age  = 26;
//方法 say
var say = function(){
            console.log("say hello");
         }
//批量導出
export {name,age,say}           
//導入 子產品B的屬性
import { name,age,say } from "./module-B.js";
console.log(name)
//列印結果:cfangxu
console.log(age)
//列印結果:26
say()
//列印結果:say hello           
import {name as myName} from './module-B.js';
console.log(myName) //cfangxu
整體導入

/使用*實作整體導入
import * as obj from "./module-B.js";

console.log(obj.name)
//結果:"cfangxu"
console.log(obj.age)
//結果:26
obj.say();
//結果:say hello           

原文連結:

繼續閱讀