Node.js中看JavaScript的引用


Posted in Javascript onApril 22, 2017

早期学习 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))。

要实现这样的热更新必须在架构上就要严格避免旧代码被引用的可能性,否则很容易写出内存泄漏的代码。

以上所述是小编给大家介绍的Node.js中看JavaScript的引用,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
javascript之dhDataGrid Ver2.0.0代码
Jul 01 Javascript
extjs DataReader、JsonReader、XmlReader的构造方法
Nov 07 Javascript
jQuery学习笔记[1] jQuery中的DOM操作
Dec 03 Javascript
JS代码优化技巧之通俗版(减少js体积)
Dec 23 Javascript
jquery实现简单的轮换出现效果实例
Jul 23 Javascript
JS实现样式清新的横排下拉菜单效果
Oct 09 Javascript
使用Ajax生成的Excel文件并下载的实例
Nov 21 Javascript
Bootstrap标签页(Tab)插件使用方法
Mar 21 Javascript
Vue中封装input组件的实例详解
Oct 17 Javascript
小程序云开发教程如何使用云函数实现点赞功能
May 18 Javascript
微信小程序request请求封装,验签代码实例
Dec 04 Javascript
JavaScript命令模式原理与用法实例详解
Mar 10 Javascript
详解Angular中的自定义服务Service、Provider以及Factory
Apr 22 #Javascript
Ionic2调用本地SQlite实例
Apr 22 #Javascript
ionic+AngularJs实现获取验证码倒计时按钮
Apr 22 #Javascript
移动端使用localResizeIMG4压缩图片
Apr 22 #Javascript
使用Xcache缓存器加速PHP网站的配置方法
Apr 22 #Javascript
AngularJS的脏检查深入分析
Apr 22 #Javascript
在node中如何使用 ES6
Apr 22 #Javascript
You might like
php面向对象全攻略 (五) 封装性
2009/09/30 PHP
php smarty模版引擎中变量操作符及使用方法
2009/12/11 PHP
PHP生成自定义长度随机字符串的函数分享
2014/05/04 PHP
PHP中的多行字符串传递给JavaScript的两种方法
2014/06/19 PHP
ThinkPHP打开验证码页面显示乱码的解决方法
2014/12/18 PHP
YII Framework框架教程之使用YIIC快速创建YII应用详解
2016/03/15 PHP
PHP调用API接口实现天气查询功能的示例
2017/09/21 PHP
jQuery之折叠面板的深入解析
2013/06/19 Javascript
JS控制图片等比例缩放的示例代码
2013/12/24 Javascript
Jquery插件编写简明教程
2014/03/25 Javascript
浅析webapp框架AngularUI的demo
2014/12/21 Javascript
JS操作JSON方法总结(推荐)
2016/06/14 Javascript
jQuery中slidedown与slideup方法用法示例
2016/09/16 Javascript
过期软件破解办法实例详解
2017/01/04 Javascript
js实现tab选项卡切换功能
2017/01/13 Javascript
基于jQuery使用Ajax动态执行模糊查询功能
2018/07/05 jQuery
微信小程序自定义头部导航栏和导航栏背景图片 navigationStyle问题
2019/07/26 Javascript
如何使用proxy实现一个简单完整的MVVM库的示例代码
2019/09/17 Javascript
原生JavaScript实现滑动拖动验证的示例代码
2019/12/06 Javascript
Python 随机生成中文验证码的实例代码
2013/03/20 Python
Python中线程编程之threading模块的使用详解
2015/06/23 Python
使用Python中的tkinter模块作图的方法
2017/02/07 Python
解决pycharm 误删掉项目文件的处理方法
2018/10/22 Python
使用11行Python代码盗取了室友的U盘内容
2018/10/23 Python
基于Numba提高python运行效率过程解析
2020/03/02 Python
jenkins+python自动化测试持续集成教程
2020/05/12 Python
pycharm配置python 设置pip安装源为豆瓣源
2021/02/05 Python
美国著名的婴儿学步鞋老品牌:Robeez
2016/08/20 全球购物
武汉某公司的C#笔试题面试题
2015/12/25 面试题
《天安门广场》教学反思
2014/04/23 职场文书
2014年人民调解工作总结
2014/12/08 职场文书
2015年办公室主任工作总结
2015/04/09 职场文书
2015年数学教师工作总结
2015/05/20 职场文书
2016大学生暑期三下乡心得体会
2016/01/23 职场文书
《原神》新角色演示“神里绫人:林隐泓洄” 宠妹狂魔
2022/04/03 其他游戏
Win11电脑显示本地时间与服务器时间不一致怎么解决?
2022/04/05 数码科技