一周的进度
为了更好地了解javascript这门我比较喜欢的编程语言,我选择了《你不知道的javascript》这一本书来进行学习,先从上卷开始学习,一周下来把第一部分”作用域和闭包”以及第二部分“this和对象原型”的一到三章学习完毕,并做了一些笔记,一周将过,准备对学习的内容进行反思.
javascript编译原理
其实第一部分第一章居然先从编译原理开始讲起我是蛮惊讶的,了解了不少新概念:
编译分为如下阶段:
1. 分词/词法分析 (Tokenizing/Lexing)
分解为有意义的代码块,成为词法单元(token)
var a = ;
=> ‘var’, ‘a’, ‘=’, ‘2’, ‘;’
2. 解析/语法分析(Parsing)
词法单元流 –> 抽象语法树(Abstract Syntax Tree, AST)
AST: 由元素逐级嵌套所组成的代表了程序语法结构的树
VariableDeclaration
|
|----------------|
| |
Identifier(a) AssignmentExpression
|
|
NumericLiteral(2)
3. 代码生成
AST –> 可执行代码
var a = 2;
–> 机器指令(创建变量a,并将一个数值存入其中)
了解了如上的概念,就引出了javascript编译运行的特点
javascript 编译特点
会在语法分析与代码生产阶段进行一定的优化
编译发生在代码执行的前几微秒
这里比较重要,想要javascript代码运行分为两个阶段,编译与运行,为后面相关关键概念的分析,埋下伏笔
作用域
在看此部分之前,我对作用域概念是:javascript用 var
声明的变量为函数级作用域,let
声明的变量为块级作用域,如果向一个未在特定作用域声明的变量赋值的话,js引擎逐级向上查找变量,直到找到位置,学习完这一章感觉我的概念理解是没错,不过细节不够,也不知道其原理如何。
此章中先引入了与js运行有关的三个名词:
相关名词
引擎: 负责js程序的编译及执行过程
编译器: 负责语法分析及代码生成
作用域: 负责所有标识符组成的一系列查询,确定访问权限
然后介绍了 var a = 2
的执行过程
一个赋值语句的执行过程
'var a' --> 查询作用域中是否有变量a
|
|
/ \
有 没有
| |
/ \
在当前作用域中 忽略该声明,
声明变量a 继续编译
| |
\ /
\ /
a = 2
|
当前作用域中是否存在a变量
|
/ \
/ \
| |
有 没有
| |
使用 继续查找
接着介绍了两种查询方式
LHS,RHS
其实就是就是赋值的时候LHS,取值的使用用RHS
LHS
变量在赋值操作左侧
试图查找变量容器本身, 从而对其赋值
赋值操作的目的是谁
a = 2
RHS
变量在赋值操作右侧
查找某个变量的值
谁是赋值的源头
console.log(a)
样例分析
function foo(a) {
console.log(a); // a LSH a <-- 2
// console RSH
// log RSH
// log <-- 2 RSH
// arg1 LSH
}
foo(2); // foo RSH
还讲到了作用域的嵌套,其实就是 当前作用域 --> .... --> 全局作用域
,如果在LSH查询一直没查询到,且不再严格模式下的话,js会在全局作用域中创建这个变量,如果是在严格模式下的话,则会抛出 ReferenceError
异常,到了这里顺便就说明了两种和变量有关的异常
异常
/*1*/ function foo(a) {
/*2*/ console.log( a + b );
/*3*/ b = a;
/*4*/ }
/*5*/ foo(2);
(2)b <– RSH : ReferenceError
(3)b <– LSH : 非严格模式 : 在全局中创建b
严格模式 : ReferenceError
找到, 但类型不对, 或为 null/undefined : TypeError
小结一
到此为止第一章就结束了,第一章的题目叫 作用域是什么
,其实就是变量所能使用的范围
词法作用域
介绍了几乎所有js书上都不建议使用的 eval
中的变量所产生的作用域,为当前执行区域的作用域,但是在严格模式下的话会有自己的作用域
eval
function foo(str, a) {
eval(str) // 欺骗
console.log(a, b) // 1, 3
}
var b = 2;
foo("var b = 3;" , 1);
在严格模式下eval有自己的词法作用域
function foo(str, a) {
"use strict"
eval(str);
console.log(a, b) // ReferenceError: b is not defined
}
foo("var b = 3;" , 1);
与 eval
相似的函数
setTimeout
setInterval
new Function
var x = 10;
function createFunction1() {
var x = 20;
return new Function('return x;'); // this |x| refers global |x|
}
var f1 = createFunction1();
console.log(f1()); // 10
with
可以快速为对象的属性赋值,但是对于对象不存在的属性,会在全局上创建一个变量,变量名为属性名,变量值为 with
函数赋的值
with
在严格模式下被禁用
function foo(obj) {
with(obj) {
a = 2;
}
}
var o1 = {a:3};
var o2 = {b:3};
foo(o1);console.log(o1.a); // 2
foo(o2);console.log(o2.a); // undefined
console.log(a); // 2
将未找到的对象的属性,进行LSH查询,随后处理为当前(顶层)作用域中的语法标识符
eval 与 with 对性能的影响
1 js引擎会在编译阶段对数据项进行优化,依赖于对词法的静态分析
2 eval() 和 with 无法进行分析,所以无法进行优化
小结二
第二章结束,介绍了编译器进行词法分析时的一个概念—词法作用域,正常的词法作用域就是根据var
对应的函数作用域或是let
对应的块级作用域来划分的,但是js中提供的with
和with
会产生一些影响,虽然人们都说这两个东西不要用,但是它们毕竟也是js的一个组成部分,了解了解还是好的。
函数作用域与块级作用域
自以为因为对和两个概念还是比较了解的,但是还是从这章里学了不少最佳实践
这里先介绍了善用两者带来的好处
1 实现私有变量
2 避免变量名冲突
作用域最佳实践一
为其添加函数名
传入全局变量
(function IIFE(global) {
.....
})(window);
作用域最佳实践二
UMD (Universal Module Definition)
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});
块级作用域的优势
- 1 垃圾回收
function process(data) { .... };
{
let someReallyBigData = { ... };
process(someReallyBigData);
}
// 销毁这个块级作用域
var btn = document.getElementById("my_btn);
btn.addEventListener("click", function click(evt) {
....
}, false);
- 2 let 循环
当然,也介绍了 const
小结三
介绍了两个作用域,为后面对模块的使用做铺垫
提升
现提出了两个代码,并对其结果提出问题
问题
a = 2;
var a;
console.log(a); // 2
console.log(a); // undefined
var a = 2;
解答
这里从一开始讲的编译原理开始解释
- 编译阶段: 对已声明的变量分配内存空间,不存在的话则在当前作用域创建
- 运行阶段: 对变量进行赋值
所以上面的代码实际应该是这样的:
var a;
a = 2;
console.log(a); // 2
var a;
console.log(a); // undefined
a = 2;
还举的另外两个例子
foo(); // <-- foo为undefined, 不能当函数用
var foo = function () {
console.log(a);
var a = 2;
}
// Uncaught TypeError: foo is not a function
bar();
var foo = function bar() {
console.log(a);
var a = 2;
}
// Uncaught ReferenceError: bar is not defined
// 在当前作用域中无法调用
// 相当于是
/**
* foo = function() {var var = ..self ..}
*/
变量提升
函数优先提升,然后才是变
举了两个例子,非常好懂
变量提升例子一
foo() // 1
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}
编译器理解的:
function foo() {
console.log(1);
}
foo() // 1
foo = function() {
console.log(2);
}
变量提升例子二
foo(); // 3
function foo() { console.log( 1 );
}
var foo = function() { console.log( 2 );
};
function foo() { console.log( 3 );
}
小结四
var a = 2;
-> 编译阶段: var a
-> 执行任务阶段: a = 2;
作用域与闭包
终于到了javascript重头戏之一了,传说中的 闭包 !!!
其实闭包吧,就是由作用域引起的,这里举了一个例子
闭包例子与概念
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
使bar()所在的内部作用域不会被内存管理器回收
bar() 依然持有对作作用域的引用, 这个引用被称为闭包
闭包使函数可以访问定义时的词法作用域
经典错误
for(var i = 0; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
其实吧,就是函数作用域搞的鬼 :-P
修正
for(var i = 0; i <= 5; i++) {
(function(tmp_i) {
setTimeout(function timer() {
console.log(tmp_i);
}, tmp_i * 1000);
})(i);
}
高级修正
for(let i = 0; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模块机制
一个例子,说明如何使用闭包模仿模块机制写出高质量的代码
function module(initVal) {
var val = initVal;
function getVal() {
return val;
}
function setVal(newVal) {
val = newVal;
}
return {
getVal: getVal,
setVal: setVal
};
}
var foo = module("aaa");
console.log(foo.getVal()); // "aaa"
foo.setVal("bbb");
console.log(foo.getVal()); // "bbb"
玩转模块
这个例子感觉有点厉害……
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("aaaaa"));
foo.awesome();
// Let me introduce: aaaaa
// LET ME INTRODUCE: HIPPO
小结五
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这是就产生了闭包。
模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
附录:this的词法
附录可以看作是承上启下的作用吧
需要掌握 this
的指向!!
this的使用问题
var obj = {
count: 0,
cool: function foolFn() {
if (this.count < 1) {
setTimeout( function timer() {
this.count++;
console.log(this.count); // undefined
}, 100);
}
}
};
obj.cool();
使用 self 缓存
var obj = {
count: 0,
cool: function foolFn() {
var self = this;
if (self.count < 1) {
setTimeout( function timer() {
self.count++;
console.log(self.count); // 1
}, 100);
}
}
};
obj.cool();
使用es6新语法
var obj = {
count: 0,
cool: function foolFn() {
if (this.count < 1) {
setTimeout(() => {
this.count++;
console.log(this.count); // 1
}, 100);
}
}
}
obj.cool();
使用 bind 绑定this
var obj = {
count: 0,
cool: function foolFn() {
if (this.count < 1) {
setTimeout(function timer() {
this.count++;
console.log(this.count); // 1
}.bind(this), 100);
}
}
};
obj.cool();
其实吧,我觉得使用 bind
更酷一点,使用尖头函数更简洁
大总结
到此为止第一部分就结束了,主要目的是将闭包以及模块的使用,但是为了讲解清楚在前面补充了好多基础概念,读完一遍真的是豁然开朗,的确是一本不可多得的好书
第二部分我打算都学习完毕后在写总结,也就是后几天吧。