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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/nzyalj/article/details/78079860

一周的进度

为了更好地了解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 编译特点

  • 会在语法分析与代码生产阶段进行一定的优化

    扫描二维码关注公众号,回复: 2959201 查看本文章
  • 编译发生在代码执行的前几微秒

这里比较重要,想要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中提供的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); // 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 更酷一点,使用尖头函数更简洁


大总结

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

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

猜你喜欢

转载自blog.csdn.net/nzyalj/article/details/78079860