這篇文章主要介紹了Node.js中看JavaScript的引用的相關資料,需要的朋友可以參考下
早期學習Node.js 的時候(2011-2012),有挺多是從PHP 轉過來的,當時有部分人對於Node.js 編輯完程式碼需要重啟一下表示麻煩(PHP不需要這個過程),於是社群裡的朋友就開始提倡使用node-supervisor 這個模組來啟動專案,可以編輯完程式碼之後自動重新啟動。不過相對於 PHP 而言依舊不夠方便,因為 Node.js 在重啟以後,之前的上下文都遺失了。
雖然可以透過將session 資料保存在資料庫或快取中來減少重啟過程中的資料遺失,不過如果是在生產的情況下,更新程式碼的重啟間隙是沒法處理請求的(PHP可以,另外那個時候Node.js 還沒有cluster)。由於這方面的問題,加上本人是從 PHP 轉到 Node.js 的,於是從那時開始思考,有沒有辦法可以在不重啟的情況下熱更新 Node.js 的程式碼。
開始把目光瞄向了 require 這個模組。想法很簡單,因為 Node.js 中引入一個模組都是透過 require 這個方法載入的。於是開始思考 require 能不能再更新程式碼後 require 一下。試試如下:
a.js
var express = require('express'); var b = require('./b.js'); var app = express(); app.get('/', function (req, res) { b = require('./b.js'); res.send(b.num); }); app.listen(3000);
#b.js
exports.num = 1024;
兩個JS 檔案寫好之後,從a.js 啟動,重新整理頁面會輸出b.js 中的1024,然後修改b.js 檔案中匯出的值,例如修改為2048。再次刷新頁面依舊是原本的 1024。
再次執行一次 require 並沒有刷新程式碼。 require 在執行的過程中載入完程式碼之後會把模組匯出的資料放在 require.cache 中。 require.cache 是一個 { } 物件,以模組的絕對路徑為 key,該模組的詳細資料為 value。於是便開始做以下嘗試:
a.js
var path = require('path'); var express = require('express'); var b = require('./b.js'); var app = express(); app.get('/', function (req, res) { if (true) { // 检查文件是否修改 flush(); } res.send(b.num); }); function flush() { delete require.cache[path.join(dirname, './b.js')]; b = require('./b.js'); } app.listen(3000);
再次require 之前,將require 之上關於該模組的cache 清理掉後,用之前的方法再次測試。結果發現,可以成功的刷新 b.js 的程式碼,輸出新修改的值。
了解到這個點後,就想透過這個原理實現一個無重啟熱更新版本的 node-supervisor。在封裝模組的過程中,出於情懷的原因,考慮提供一個類似 PHP 中 include 的函數來取代 require 去引入一個模組。實際內部依舊是使用 require 去載入。以b.js為例,原本的寫法改為var b = include('./b')
,在檔案b.js 更新之後include 內部可以自動刷新,讓外面拿到最新的程式碼。
但是在實際的開發過程中,這樣很快就碰到了問題。我們希望的程式碼可能是這樣:
web.js
var include = require('./include'); var express = require('express'); var b = include('./b.js'); var app = express(); app.get('/', function (req, res) { res.send(b.num); }); app.listen(3000);
但按照這個目標封裝include的時候,我們發現了問題。無論我們在include.js內部中如何實現,都不能像開始那樣拿到新的 b.num。
對比開始的程式碼,我們發現問題出在少了 b = xx。也就是說這樣寫才可以:
web.js
var include = require('./include'); var express = require('express'); var app = express(); app.get('/', function (req, res) { var b = include('./b.js'); res.send(b.num); }); app.listen(3000);
修改成這樣,就可以保證每次能可以正確的刷新到最新的程式碼,而且不用重啟實例了。讀者有興趣的可以研究這個include是怎麼實現的,本文就不深入討論了,因為這個技巧使用度不高,寫起起來不是很優雅[1],反而這其中有一個更重要的問題——JavaScript的引用。
JavaScript 的引用與傳統引用的區別
#要討論這個問題,我們首先要了解JavaScript 的引用於其他語言中的一個區別,在C++ 中引用可以直接修改外部的值:
#include using namespace std; void test(int &p) // 引用传递 { p = 2048; } int main() { int a = 1024; int &p = a; // 设置引用p指向a test(p); // 调用函数 cout << "p: " << p << endl; // 2048 cout << "a: " << a << endl; // 2048 return 0; }
而在JavaScript 中:
var obj = { name: 'Alan' }; function test1(obj) { obj = { hello: 'world' }; // 试图修改外部obj } test1(obj); console.log(obj); // { name: 'Alan' } // 并没有修改① function test2(obj) { obj.name = 'world'; // 根据该对象修改其上的属性 } test2(obj); console.log(obj); // { name: 'world' } // 修改成功②
我們發現與C++ 不同,根據上面程式碼① 可知JavaScript 中並沒有傳遞一個引用,而是拷貝了一個新的變數,即值傳遞。根據 ② 可知拷貝的這個變數是一個可以存取到物件屬性的「引用」(與傳統的 C++ 的引用不同,下文中提到的 JavaScript 的引用都是這種特別的引用)。這裡需要總結一個繞口的結論:Javascript 中都是值傳遞,物件在傳遞的過程中是拷貝了一份新的引用。
為了理解這個比較拗口的結論,讓我們來看一段程式碼:
var obj = { name: 'Alan' }; function test1(obj) { obj = { hello: 'world' }; // 试图修改外部obj } test1(obj); console.log(obj); // { name: 'Alan' } // 并没有修改① function test2(obj) { obj.name = 'world'; // 根据该对象修改其上的属性 } test2(obj); console.log(obj); // { name: 'world' } // 修改成功②
通过这个例子我们可以看到,data 虽然像一个引用一样指向了 obj.data,并且通过 data 可以访问到 obj.data 上的属性。但是由于 JavaScript 值传递的特性直接修改 data = xxx 并不会使得 obj.data = xxx。
打个比方最初设置 var data = obj.data 的时候,内存中的情况大概是:
| Addr | 内容 | |----------|-------- | obj.data | 内存1 | | data | 内存1 |
所以通过 data.xx 可以修改 obj.data 的内存1。
然后设置 data = xxx,由于 data 是拷贝的一个新的值,只是这个值是一个引用(指向内存1)罢了。让它等于另外一个对象就好比:
| Addr | 内容 | |----------|-------- | obj.data | 内存1 || data | 内存2 |
让 data 指向了新的一块内存2。
如果是传统的引用(如上文中提到的 C++ 的引用),那么 obj.data 本身会变成新的内存2,但 JavaScript 中均是值传递,对象在传递的过程中拷贝了一份新的引用。所以这个新拷贝的变量被改变并不影响原本的对象。
Node.js 中的 module.exports 与 exports
上述例子中的 obj.data 与 data 的关系,就是 Node.js 中的 module.exports 与 exports 之间的关系。让我们来看看 Node.js 中 require 一个文件时的实际结构:
function require(...) { var module = { exports: {} }; ((module, exports) => { // Node.js 中文件外部其实被包了一层自执行的函数 // 这中间是你模块内部的代码. function some_func() {}; exports = some_func; // 这样赋值,exports便不再指向module.exports // 而module.exports依旧是{} module.exports = some_func; // 这样设置才能修改到原本的exports })(module, module.exports); return module.exports; }
所以很自然的:
console.log(module.exports === exports); // true // 所以 exports 所操作的就是 module.exports
Node.js 中的 exports 就是拷贝的一份 module.exports 的引用。通过 exports 可以修改Node.js 当前文件导出的属性,但是不能修改当前模块本身。通过 module.exports 才可以修改到其本身。表现上来说:
exports = 1; // 无效 module.exports = 1; // 有效
这是二者表现上的区别,其他方面用起来都没有差别。所以你现在应该知道写module.exports.xx = xxx; 的人其实是多写了一个module.。
更复杂的例子
为了再练习一下,我们在来看一个比较复杂的例子:
var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x); console.log(b.x);
按照开始的结论我们可以一步步的来看这个问题:
var a = {n: 1}; // 引用a指向内存1{n:1} var b = a; // 引用b => a => { n:1 }
内部结构:
| Addr | 内容 | |---------|-------------| | a | 内存1 {n:1} | | b | 内存1 |
继续往下看:
a.x = a = {n: 2}; // (内存1 而不是 a ).x = 引用 a = 内存2 {n:2}
a 虽然是引用,但是 JavaScript 是值传的这个引用,所以被修改不影响原本的地方。
| Addr | 内容 | |-----------|-----------------------| | 1) a | 内存2({n:2}) | | 2) 内存1.x | 内存2({n:2}) | | 3) b | 内存1({n:1, x:内存2}) |
所以最后的结果
a.x 即(内存2).x ==> {n: 2}.x ==> undefined b.x 即(内存1).x ==> 内存2 ==> {n: 2}
总结
JavaScrip t中没有引用传递,只有值传递。对象(引用类型)的传递只是拷贝一个新的引用,这个新的引用可以访问原本对象上的属性,但是这个新的引用本身是放在另外一个格子上的值,直接往这个格子赋新的值,并不会影响原本的对象。本文开头所讨论的 Node.js 热更新时碰到的也是这个问题,区别是对象本身改变了,而原本拷贝出来的引用还指向旧的内存,所以通过旧的引用调用不到新的方法。
Node.js 并没有对 JavaScript 施加黑魔法,其中的引用问题依旧是 JavaScript 的内容。如 module.exports 与 exports 这样隐藏了一些细节容易使人误会,本质还是 JavaScript 的问题。
注[1]:
老实说,模块在函数内声明有点谭浩强的感觉。
把 b = include(xxx) 写在调用内部,还可以通过设置成中间件绑定在公共地方来写。
除了写在调用内部,也可以导出一个工厂函数,每次使用时 b().num 一下调用也可以。
还可以通过中间件的形式绑定在框架的公用对象上(如:ctx.b = include(xxx))。
要实现这样的热更新必须在架构上就要严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。
以上是JavaScript的引用在Node.js中的具體介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!