《你不知道的javascript》阅读笔记(上卷第一部分)

来源:互联网 发布:java什么是逻辑或 编辑:程序博客网 时间:2024/04/28 15:34

一周的进度

为了更好地了解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); // 2foo(o2);console.log(o2.a); // undefinedconsole.log(a);            // 2

将未找到的对象的属性,进行LSH查询,随后处理为当前(顶层)作用域中的语法标识符

eval 与 with 对性能的影响

  • 1 js引擎会在编译阶段对数据项进行优化,依赖于对词法的静态分析

  • 2 eval() 和 with 无法进行分析,所以无法进行优化


小结二

第二章结束,介绍了编译器进行词法分析时的一个概念—词法作用域,正常的词法作用域就是根据var对应的函数作用域或是let对应的块级作用域来划分的,但是js中提供的withwith会产生一些影响,虽然人们都说这两个东西不要用,但是它们毕竟也是js的一个组成部分,了解了解还是好的。


函数作用域与块级作用域

自以为因为对和两个概念还是比较了解的,但是还是从这章里学了不少最佳实践

这里先介绍了善用两者带来的好处

  • 1 实现私有变量

  • 2 避免变量名冲突

作用域最佳实践一

  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); // undefinedvar a = 2;

解答

这里从一开始讲的编译原理开始解释

  • 编译阶段: 对已声明的变量分配内存空间,不存在的话则在当前作用域创建
  • 运行阶段: 对变量进行赋值

所以上面的代码实际应该是这样的:

var a;a = 2;console.log(a); // 2
var a;console.log(a); // undefineda = 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() // 1var foo;function foo() {  console.log(1);}foo = function() {  console.log(2);}

编译器理解的:

function foo() {  console.log(1);}foo() // 1foo = function() {  console.log(2);}

变量提升例子二

foo(); // 3function 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 更酷一点,使用尖头函数更简洁


大总结

到此为止第一部分就结束了,主要目的是将闭包以及模块的使用,但是为了讲解清楚在前面补充了好多基础概念,读完一遍真的是豁然开朗,的确是一本不可多得的好书

第二部分我打算都学习完毕后在写总结,也就是后几天吧。

阅读全文
0 0
原创粉丝点击