带你精读你不知道的Javasript(上)(一)

时间:2024-01-21 21:50:34

斌果在这几天看了下你不知道的js这本书,这本书讲的东西还是挺不错的,其中有很多平时我压根没接触到的概念和方法。借此也可以丰富一下我对js的了解。

第一部分

第一章 作用域是什么?

1.程序中一点源代码在执行之前会经历以下三个步骤,统称为“编译”。

分词/词法分析:

这个过程将由字符组成的字符串分解成有意思的代码块(词法单元)。例如 var a = 2;会被分解为var、a、=、2、;。空格是否被分解取决于其是否有意义。

解析/语法分析:

这个过程会讲单元流(数组)转换成“抽象语法书”。就如将一条语句拆分成词法单元放分别放到一课分层树上。

代码生成:

将AST转换为课执行的代码。

 2.编译器中的术语(LHS.RHS)

其中最主要介绍的两个查询 LHSRHS查询

LHS:赋值操作的目标是谁;(可以理解为要找到目标进行操作)

RHS:谁是赋值操作的源头;(可以理解为查找是否有这个目标)

//下面看一个例子
function foo(a){
console.log(a); //
} foo(2);

在这个例子中foo进行RHS引用,看下作用域中是否定义了foo函数;然后再为a进行LHS引用,将a赋值为2;接着对console进行RHS引用,看下作用域中是否有console这个内置

对象是否有log方法;最后再对a进行RHS引用。LHS和RHS引用会在作用域中一层一层向上找,如何抵达顶层(全局作用域)还没找到,就会停止。

//考虑一下这段代码

function foo(a){
console.log(a + b);
b = a;
} foo(2);

在这段代码中,当对b进行RHS引用时,在作用域中无法找到该变量,引擎会抛出ReferenceError异常。相比之下LHS在非严格模式下是不会抛出异常而是隐式地创建一个全局变量。

而在严格模式下他也会抛出和ReferencError异常。

第二章 词法作用域

1.词法作用域

简单来说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和作用域写在哪里来决定的,一般情况下词法作用域在词法分析器处理代码时

会保持作用域不变。

//分析一下下面这段代码

function foo(a){
var b = 1 * 2;
function bar(c){
console.log(a,b,c);
}
bar(b*3);
} foo(2); //2,4,12

在这个例子中一共有三层嵌套作用域。(灰色、浅蓝色、浅黄色),其中又外到内是逐层包含关系。没有一个作用域能够同时出现在两个外部作用域中!就如没有任何函数可以部分地同

时出现在两个父级函数中一样。作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”;

2.欺骗词法

javascript中有两种机制可以实现这个目的,但是欺骗词法作用域是会付出相应的代价,那就是会导致性能的下降。

1.eval

eval()函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序这个位置的代码。

在执行eval()之后的代码是,引擎并不“知道”或“在意”前面的代码时以动态形式插入进来,并对词法作用域的环境是进行修改的。

//让我们来看一下这段代码

function foo(str,a){
eval(str);
console.log(a,b);
}
var b = 2; foo("var b = 3;",1); //1,3

这段代码中foo会在内部找到变量a和b,而遮蔽了外部的全局变量b,所以会输出1,3.

无论在何种情况下,eval()都可以在运行期修改书写期的词法作用域。

2.with

with通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重负应用对象本身。

//例如下面这个例子

var obj = {
a: 1,
b: 2,
c: 3
}; //单调乏味的重复“obj”
obj.a = 2;obj.b = 3;obj.c = 4; //简单的快捷方式
with(obj){
a = 3;
b = 4;
c = 5;
}

然而其作用不单单这样,再来看下下面的代码。

function foo(obj) {
with(obj){
a = 2;
}
} var o1 = {
a:3
}; var o2 = {
b:3
}; foo(o1);
console.log(o1.a); // foo(o2);
console.log(o2.a); //undefind
console.log(a); //2--------不好,a被泄漏到全局作用域上了!

在with块内部,看起来只是对变量a进行了简单的词法引用,实际上就是一个LHS引用,由于o2中没有a属性,其会隐式创建一个全局变量a。

总结:

eval()函数如果接受了函数一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象凭空创建一个全新的词法作用域。

然而,最悲观的是如果代码中出现eval()或with,引擎只能简单假设关于标识符位置的判断是无效的,因为无法预测其会接受到什么。从而可能导致引擎中所有的优化都是无意义的,因此最简单的做法就是其完全不做任何优化。所以我们不要使用它们。

第3章 函数作用域和块作用域

1.函数作用域

函数作用域的含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

function foo(a) {
var b = 2;
function bar() {
//.....
}
var c = 3;
} bar(); //失败
console.log(a, b, c); //访问失败

其中(a、b、c、foo和bar)在foo()中是可以访问的,而这些标识符无法从全局作用域中进行访问。

2. 隐藏内部实现

“隐藏”作用域中的变量和函数能将具体内容私有化,并且能避免同名标识符之间的冲突。

//下面看一下同名冲突例子

function foo(){
function bar(a){
i = 3; //修改for循环所属作用域中的i
console.log(a + i);
}
for(var i=0;i<10;i++){
bar(i * 2); //糟糕,无限循环了!
}
}

虽然我们可以修改bar中i的名字就可以解决这个问题,但软件设计在某种情况下可能自然而然地要求使用同样的标识符名称,因此,使用

“隐藏”内部声明是唯一的最佳途径。

var a = 2;
function foo(){ //添加这一行
var a = 3;
console.log(a); //
} //以及这一行
foo(); //和这一行 console.log(a); //

这样虽然能“隐藏”内部的变量和函数定义,但是,其又多了一个foo的全局函数污染了作用域,所以,我们可以采用下面这种方案:

var a = 2;
(function () { //添加该行
var a = 3;
console.log(a);
})(); //还有该行 console.log(a); //

这样,函数会被当做函数表达式而不是一个标准的函数声明来处理。

3.匿名和具名函数

函数表达式可以是匿名,而函数声明则不可以省函数名---在js中这是非法的。、

在很多场合我们都喜欢用匿名函数,这样显得简便而高大上,但还要考虑到一下的缺点

1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。

2.如果没有函数名,当函数需要引用自身是只能使用已经过期的arguments.callee引用,

比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。

3.匿名函数省略了对代码的可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。

所以,我们应该尽量使用具名函数而少或不使用匿名函数

4.立即执行函数表达式

IIFE:代表立即执行表达式;

立即执行表达式非常常见,而且它还能“隐藏”函数的定义和声明。

var a = 2;
(function IIFE(s) {
var a = 3;
console.log(a+s); //3oh
})("oh"); console.log(a); //

因为函数被一对()包裹,因此成为了一个表达式,而末尾加上一对()可以

立即执行这个函数。IIFE的一个非常普遍的进阶用法是把它们当做函数调用并传递参数。

5.块作用域

相信大家对块作用域一点也不陌生,在c/java都支持块作用域。

for(var i=0;i<10;i++){
console.log(i);
};

在for循环头部定义了变量i,通常只想在for循环中上下文使用,然而在js中,并没有块作用域!这以为着,在外部也可以调用到变量i。

但是我们可以通过下属方法创建块作用域:

with: 虽然with会严重影响程序的性能,但其确实可以创建一个块作用域。

try/catch:在ES3规范中catch*会创建一个块作用域,其中声明的变量仅在catch内有效。

let:这是ES6的语言,其可以为声明的变量隐式地劫持了所在的块作用域。

const:同样可以用来创建块作用域变量,但其值是固定的。

块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。

 

第四章 提升

//首先先上一段代码

a = 2;
var a;
console.log(a); //

在javascript中所有声明本身会被提升,而复制或者其他运行逻辑会留在原地。就像下面这段代码

foo();

function foo(){
console.log(a); //undefined
var a = 2;
}

代码中foo函数声明会被提升(这个例子还包括实际函数的隐含值),函数体中的a声明也会被提升看,但a中的LHS引用却会留在原地。

foo(); //TypeError
bar(); //ReferenceError
var foo = function bar(){
//...........
};

可以看到当函数是表达式时候,函数名虽然被提升了,但表达式仍然留在原地,这会使得foo报类型错误,而bar直接是表达式的内容从而直接报错!

需要注意的是函数会首先被提升,然后才是变量。

第5章 作用域闭包

1.对闭包的理解

当函数可以记住并访问所在的词法作用域时,就产生了闭包。

这个理解起来比较难讲,还是引用百度百科上的定义吧!既:闭包就是能够读取其他函数内部变量的函数

闭包其实无处不在,举个简单的例子:

function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
} var baz = foo();
baz(); //2---这就是闭包的效果。

函数bar()的词法作用域能够访问foo()的内部作用域,这就是闭包!当然,无论使用何种方式对函数类型的值进行传递,当函

数在别处被调用时都可以观察到闭包。

function foo(){
var a = 2;
function baz(){
console.log(a); //
}
bar(baz);
} function bar(fn){
fn(); //老弟快看,这就是闭包!
} foo();

要说明闭包,for循环是最常见的例子。

for(var i=1;i<=5;i++){
setTimeout(function timer() {
console.log(i);
},i*1000);
}

按照我们的预期,是分别输出1到5,每秒一次,每次一个,但实际上,这段代码在运行时以每秒一个的频率输出5个6。

这是因为延时函数的回调函数会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(...,0),

所有的回调依然在循环结束后才会被执行,因此会输出6。尽管循环中五个函数是在各个迭代中分别定义的,但是他们都被

封闭在一个共享的全局作用域中,因此实际上只有一个i。

因此我们可以通过传参的IIFE来迭代自己的变量。

for(var i =1;i<=5;i++){
(function (j) {
setTimeout(function timer() {
console.log(j);
},j*1000)
})(i)
}

这样每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部。

 2.模块

模块就是利用了闭包的强大威力,但从表面上看,它们似乎与回调无关。

function foo() {
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 bar = foo();
bar.doSomething(); //cool
bar.doAnother(); //1!2!3

这个模式在javascript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露

简单的描述,模块模式需要具备两个必要条件:

1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)

2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中星城闭包,并且可以访问或者修改私有又的状态。

只有数据属性而没有闭包函数的对象并不是真正的模块。

而ES6的模块没有“行内”格式,必须被定义在一个独立的文件中。通过module.export将文件抛出,主文件通过import将一个模块中的

一个或多个API导入到当前作用域中,并分别绑定在一个变量上。module会讲整个模块的API导入并绑定到一个变量上。

---------------------由于篇幅有限,不想写太长,不方便看。第二部分后续会更新,因为近期时间有点紧所以可能更新会有点迟。