《你不知道的js(上卷)》笔记1(基础知识和闭包)
大部分接触js应该都是先用了再学,其实大部分学习大部分语言都应该采取这种方式。因为只看不练,还能入门,而如果先看的话,估计很容易就学不下去了。所以呀,要想欺骗别人,首先得欺骗自己(假装自己会了,然后开始动手,不会的再去学,然后去骗骗别人,如果觉得骗不过,再老老实实看看视频和书籍,做做笔记,周而复始,然后就真的可以骗到别人了)。^0^
首先,读本书让我了解到js的最重要的两个知识点——闭包还有this指向,其次一点的就是编译原理和对象原形。
这里记录一下闭包的相关知识。了解闭包前还需要先理解js编译原理、变量查询以及作用域。
1.基础知识
1.1 编译原理
尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。它不是提前编译的,比起传统编译语言的编译器,JavaScript 引擎要复杂得多。
对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒的时间内。任何 JavaScript 代码片段在执行前都要进行编译,然后再执行。
关于var a = 2;
的编译过程:
-
遇到 var a,检查变量名称是否存在于同一作用域,存在则忽略,否则声明新的变量a;
-
生成运行时所需的代码,用来处理
a = 2
赋值操作;
执行代码时,引擎会去查找变量a, 如果查找到,就会进行赋值,否则就会抛出异常。
1.2 关于变量的查找
变量查询分为LHS查询
和RHS查询
,上面赋值操作将进行LHS查询
。
当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
赋值操作的目标是谁
LHS
以及谁是赋值操作的源头RHS
。
LHS查询
是试图找到变量的容器本身,从而可以对其赋值。RHS查询
相当于查找某个变量的值,RHS查询
并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。
如console.log( a );
,其中对 a 的引用是一个 RHS 引用,而a = 2;
对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么。
拓展
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
这里LHS查询
有3处,RHS查询
有4处,foo方法调用也需要一次RHS查询
, 参数传递需要将2
赋值给方法形式参数a
。
1.3 关于作用域
作用域
是根据名称查找变量的一套规则。通常需要同时顾及几个作用域
。
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套
。在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的(上一级)作用域
中继续查找,直到找到该变量, 或抵达最外层的作用域
(也就是全局作用域
)为止。
如果RHS查询
未找到所需的变量,引擎就会抛出ReferenceError
异常。
当引擎执行LHS查询
时,如果在全局作用域中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是在非 “严格模式”下。
如果RHS查询
成功,但对变量进行不合理的操作时,就会抛出TypeError
异常。
遮蔽效应
作用域查找会在找到第一个匹配的标识符时停止。
全局变量会自动成为全局对象(比如浏览器中的 window 对象)的属性,可以通过全局对象访问该变量:window.a
;但无论如何无法访问到被遮蔽非全局的变量。
欺骗词法
function foo(str, a) {
eval( str ); // 欺骗! console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
使用eval
,在foo方法声明变量b
并赋值,将遮蔽全局变量b
。
在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其 中的声明无法修改所在的作用域。
with用法
var obj = { a: 1, b: 2 };
foo(obj){
with (obj) {
a = 3;
b = 4;
c = 5;
}
}
foo(obj)
console.log(obj.a) // 3
console.log(obj.c) // undefine
console.log(c) // 5
1.4 函数的作用域
函数作用域的是指,属于这个函数的全部变量都可以在整个函数的范围内(包括嵌套的作用域中)使用及复用。
最小授权或最小暴露原则:在软件设计中,应该最小限度地暴露必 要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API 设计。
作用域的好处:
规避冲突
全局命名空间易与第三方库发生变量冲突。
利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中,这样可以有效规避掉所有的意外冲突。
var a = 2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
函数会被当作函数表达式而不是一 个标准的函数声明来处理。
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位 置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中 的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
匿名函数表达式
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );
函数表达式可以没有名称标识符,而函数声明则不可以省略函数名。
匿名函数表达式有一下几个缺点:
-
匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
-
当函数需要引用自身时只能使用已经过期的arguments.callee引用, 比如在递归中。以及在事件触发后事件监听器需要解绑自身。
-
影响代码可读性。
推荐具名写法:
setTimeout( function timeoutHandler() {
console.log( "I waited 1 second!" );
}, 1000 );
立即执行函数表达式(IIFE)
var a = 2;
(function foo(a) {
a += 3;
console.log( a ); // 5
})(a);
console.log( a ); // 2
优点:
- 将外部对象作为参数,并将变量命名为任何你觉得合适的名字。有助于改进代码风格。
- 解决 undefined 标识符的默认值被错误覆盖导致的异常。
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE( undefined ) {
var a;
if (a === undefined) {
console.log( "Undefined is safe here!" );
}
})();
- 倒置代码的运行顺序,将需要运行的函数放在第二位。
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3 console.log( global.a ); // 2
});
1.5 块作用域
表面上看 JavaScript 并没有块作用域的相关功能。
for (var i=0; i<10; i++) {
console.log( i );
}
这里i
会被绑定在外部作用域(函数或全局)中。
块作用域的用处: 变量的声明应该距离使用的地方越近越好,并最大限度地本地化。
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息
当使用 var
声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。
块作用域的例子:
-
with
关键字的结构就是块作用域。 -
try/catch
的catch
分句会创建一个块作用域,其中声明的变量仅在catch
内部有效。 -
let
关键字可以将变量绑定到所在的任意作用域中。其声明的变量隐式地了所在的块作用域。 -
const
关键字同样可以用来创建块作用域变量,但其值是固定的(常量)。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log( a ); // 3
console.log( b ); // ReferenceError
let
关键字的作用:
-
let
进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。 - 和闭包及回收内存垃圾的回收机制相关。
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
let someReallyBigData = { .. };
process( someReallyBigData );
}
-
for循环
头部的 let 不仅将i
绑定到了for
循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。
for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError
1.6 提升
例1.6.1:
a = 2;
var a;
console.log( a ); // 2
例1.6.2:
console.log( a ); // undefine
var a = 2;
当你看到
var a = 2;
时,可能会认为这是一个声明。但实际上会将其看成两个声明:var a;
和a = 2;
。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动” 到了最上面。这个过程就叫作提升。
函数声明和变量声明都会被提升。但是一个值得注意的细节是函数会首先被提升,然后才是变量。
例1.6.3:
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
例1.6.4:
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}
例1.6.5:
foo(); // "b"
var a = true; if (a) {
function foo() {
console.log("a"); }
}
else {
function foo() {
console.log("b");
}
}
尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
2.闭包
JavaScript中闭包无处不在,你只需要能够识别并拥抱它。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意 识地创建闭包。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
bar()
对 a
的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。但根据前面的定义,这并不是闭包。
下面一段代码,清晰地展示了闭包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包的效果。
函数 bar()
的词法作用域能够访问foo()
的内部作用域。然后我们将bar()
函数本身当作 一个值类型进行传递。
理解闭包
在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁。事实上内部作用域依然存在(由于bar()
本身在使用),因此没有被回收。
拜bar()
所声明的位置所赐,它拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一 直存活,以供bar()
在之后任何时间进行引用。
bar()
依然持有对该作用域的引用,而这个引用就叫作闭包。
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
timer
具有涵盖wait(..)
作用域的闭包,因此还保有对变量message
的引用。
wait(..)
执行 1000 毫秒后,它的内部作用域并不会消失,timer
函数依然保有wait(..)
作用域的闭包。
循环和闭包
例2.1:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i ); // 6 6 6 6 6
}, i*1000 );
}
例2.2:
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j ); // 1 2 3 4 5 6
}, j*1000 );
})();
}
例2.3:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j ); // 1 2 3 4 5
}, j*1000 );
})( i );
}
例2.1: 根据作用域的工作原理,尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
。由于函数延迟执行,最终循环执行完才调用,得到i
的值为6。
例2.2:匿名函数有自己的作用域,变量j
用来在每个迭代中储存i
的值。
例2.3:对例2.2代码的改进。
在迭代内使用IIFE
会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
2.1 模块
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在 JavaScript 中被称为模块。我们保持内部数据变量是隐 藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共 API。
模块模式的两个必要条件:
-
必须有外部的封闭函数,该函数必须至少被调用一次。
-
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
单例模式
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
加载器 / 管理器
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
};
})();
这段代码的核心是modules[name] = impl.apply(impl, deps)
。为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中。
使用模块:
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
var bar = MyModules.get( "bar" );
console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo
总结
学而时习之,不亦说乎。看了一遍书籍,然后通过记笔记,又回顾了一遍,一些不懂的知识这次就弄懂了,同时还发现了一些漏过的知识。
很久以前,隔壁班的某某每套卷子都要重复做上6遍,然后每次成绩都排列前茅。而他的母亲却是我的班主任,虽然没有从老师那里学到这种学习方式,但是老师却我觉得学习也是一件快乐的事情,我很庆幸遇到这样一位老师。