You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
var MyModules = (function Manager () {
var modules = {};
function define (name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get (name) {
return modules[name];
}
return {
define: define,
get: get
}
})();
上面实现了一个简单的模块加载器,下面是使用它来定义模块
MyModules.define("bar", [], function () {
function hello (who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
MyModules.define("foo", ["bar"], function (bar) {
var hungry = "hippo";
function awesome () {
console.log(bar.hello(hungry).toUpperCase())
}
return {
awesome: awesome
};
});
var bar = MyModules.get("bar");
var foo = MyModules.get("foo");
console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
写在前面
对于一个前端开发者,应该没有不知道作用域的。它是一个既简单有复杂的概念,简单到每行代码都有它的影子,复杂到写过很多的代码依然不一定能完全理解。
最近在看《你不知道的JavaScript》,一直都想看看这本被捧上天的书到底写了些什么,然而并没有期待的那么多惊喜,或许是因为网上充斥着大量类似的文章吧,大概一年前看可能感触会多点。但是,看完之后不写点什么,好像看的意义就不大了。今天就花点时间,从最简单又复杂的作用域开始缕一缕。
先来份关于《你不知道的JavaScript》第一部分作用域和闭包的目录感受一下:
开始之前
先问自己几个问题
对以后再来翻看的自己说一声,概念都很清晰的话,不要浪费时间在看了....
作用域是什么
在讲作用域之前,我们先来看看什么是编译。在传统编译语言的流程中,编译分为3步:
JS是一门解释型语言,它的编译过程不是发生在构建之前,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内,所以在作用域的背后,JS引擎用尽了各种办法来保证性能最佳。
简单了解了编译后,我们再来看看作用域是什么:
上面的话不是很好理解,我们来分解一下下面的代码:
熟悉JS的同学都知道,包括变量和函数在内的所有声明都会有一个提升的过程,即首先被处理,但是赋值不会提升。所以上面的代码会被如下处理:
var a
时,编译器会询问作用域是否存在名称为a的变量。如果存在,则忽略声明,继续编译;如果不存在,则声明一个新变量,命名a。(var b;
同理)a = 2
时,引擎会询问作用域,当前作用域中是否存在该变量,如果找不到,就向上一级查找,当抵达最外层的全局作用域时,无论找到还是没找到,查找都会停止,找到了就赋值2给它,没找到就抛出异常。其中,
a = 2
这一过程中对a的查找被称为LHS查询,在b = a
这一句中,引擎会查找变量a的值和变量b的容器,并将a的值赋值给b,这一查找变量a的值被称为RHS查询。词法作用域
作用域有两种工作模式,一种词法作用域,一种动态作用域。词法作用域(也叫静态作用域)就是在词法阶段的作用域,词法分析阶段就确定了,不会改变。JS采用的就是词法作用域,但是可以通过一些欺骗词法作用域的方法,在词法分析过后依然可以修改作用域。
词法作用域与动态作用域
我们先来看看词法作用域与动态作用域的区别(因为JS采用的是词法作用域,所以对动态作用域不做过多介绍):
熟悉JS的同学应该都知道通过RHS引用到了全局作用域中的a,所以输出2。但是如果JS是动态作用域,情况就不一样了,当
foo()
无法找到a的变量引用时,会在调用foo()
的地方查找a,而不是在嵌套的词法作用域上查找,所以会输出3。下面我们来看看什么是欺骗词法。
欺骗词法作用域
JS有两个机制可以欺骗词法作用域:
eval()
和with。大多数情况下,它们是不被推荐使用的,因为欺骗词法作用域导致引擎无法在编译时对作用域查找进行优化,所以会导致性能下降;另外,在严格模式下,with被完全禁止使用,间接或非安全的使用eval也被禁止。eval()
这个方法接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。可以看到
eval()
调用了var b =3;
导致修改了原本的作用域。with 通常被当作重复引用同一个对象中的多个属性的快捷方式。
with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域
声明提升
前面有个例子说到了声明提升,我们已经知道引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来,这也正词法作用域的核心内容。声明提升在代码中比较常见,相信面试过的朋友肯定对它非常熟悉了,但是它也有几个必须要注意的点:
闭包
闭包到底是什么?
关于闭包的解释,网上有很多,《你不知道的JavaScript》的作者也给出了他的定义:
看起来不是那么生动的解释,仔细看看好像也不是很难理解,不过,作为一个程序员,代码才是王道
函数bar()的词法作用域可以访问foo()的内部作用域,foo()执行之后,bar()依然持有对foo()内部作用域的引用(也就不会被垃圾回收机制回收),bar()对该作用域的这个引用就被叫做闭包。
再来看看下面这段代码,有没有很熟悉的感觉,看过一些面试题的朋友应该都不会陌生吧,答案是每隔一秒的频率输出五次6
浏览器运行机制,任务队列之类的我们就不讨论了,我们来看看怎么改进,从闭包的角度出现让它输出1~5
上面的代码可以吗?是的,不行,我们只是封闭了什么都没有的空作用域中,依然会向上查找全局的i。怎么实现?写了这么多,我的任务完成了,轮到你动一下脑瓜子了。
模块依赖加载器
require(['a', 'b'], callback)
这样的模块加载方式有没有勾起你老人家什么回忆呢?作为一个年轻人,ES6的import
大法还是比较适合我,不过前辈当时的先进经验还是有很多可以学习的地方的,直接贴代码了上面实现了一个简单的模块加载器,下面是使用它来定义模块
写在最后
不知不觉,篇幅已经不短,怎么总结的更精炼确实是一个技术活,我得好好学学才行。本篇主要从编译、词法作用域、声明提升的角度对JS的作用域进行了介绍,并慢慢打开了闭包的大门,最后展示了一个简单的模块加载器的代码。
The text was updated successfully, but these errors were encountered: