你不懂的JS学习笔记(做用域和闭包)

You don't KnowJS

引语:你不懂的JS这本书github上已经有了7w的star最近也是张野大大给我推荐了一波,阅读过以后感受对js的基础又有了更好的理解。原本我是历来不这种读书笔记的,可是这本书的内容实在是太多太多哪哪都是重点。因此
也就决定记录如下重要的地方便于之后复习方便若是有错误,感谢指出html

第一部分:做用域和闭包

第一章

编译的三个步骤(固然也就是编译器干的事情了)

  1. 分词/词法分析
    通俗来讲就是编译器会将咱们写的代码首先拆分红能够进行编译的代码 eg:var a = 2;能够被编译器分割为var,a,=,2,; 空格是否会被看成词法单元,取决于空格在这门语言中是否具备意义。
  2. 解析/语法分析
    AST:抽象语法树的概念他会把上述分割好的代码进组装成为一个语法树m,=,var,a,2 都回变成语法树的各个节点,从而为编译作准备。
  3. 代码生成
    编译器最终会将这样的AST语法树编译为可执行的底层代码。特别要强调的是JS的引擎在编译器执行是会帮助编译器作代码优化,同一般来讲他不会编译的过程就发生在引擎执行代码的前很短的时间,并非像执行C/C++等这些代码须要先build完整个文件再进行run这样的方式。

理解做用域 (一般指的是词法做用域或者也能够叫作静态做用域)

首先说一下基本的执行顺序首先是编译器由上面的步骤编译代码而后对于一些变量的声明会在编译期间交给做用域而后做用域就会组成一个
像是一个树的结构,全局做用域下面会有嵌套的函数做用域。最后JS引擎根据做用域去执行代码,大概就是这样的一个流程。
介绍如下三个关键的概念: 前端

  1. 编译器: 用来在引擎执行代码前提供给引擎代码而且向做用域提供组成“树”的节点
  2. 引擎:用来负责执行和编译的环境 配合做用域组成本身的上下文
  3. 做用域:负责收集并维护由全部声明的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。
    LHS和RHS:“赋值操做的目标是谁(LHS)”以及“谁是赋值操做的源头(RHS)”。PS:rhs参考对象为语句中的常量例如:console.log(1)就是谁来对于1进行操做,lh参考对象为语句中的常量例如:a = 22应该赋值给谁若是是 console.log(a)应该就是RHS和LHS一块儿

引擎和做用域的关系

下面是我写的书上的题
测验的答案:LHS:foo->c,2->a,a->b
ps:1. 能够理解为foo须要知道本身应该赋值给谁因此LHR
ps:2. 能够理解为2须要知道本身赋值给谁这里是foo的参数a
ps:3. 能够理解为a须要给谁赋值
测试的答案:RHS:2->foo,a->foo,b->a,a+b->return
ps: 1. 能够理解为是谁调用2,因此是foo
ps:2. 同上能够知道后续的三个答案git

做用域的嵌套

做用域的嵌套:做用域是个家族,爸爸认识儿子的人,爷爷认识爸爸认识的人,每次问儿子有没有有认识的人,若是没有再问爸爸。 也就是
上文提到的树结构
ReferenceError:你找了做用域整个家族都不认识的人就会出错,并无申明这个引用了没有声明的变量的错误.
LHS查询的时候须要特别注意的是 若是LHS在全局做用域当中都没法找到变量就会建立一个变量(非严格模式)
若是查找的目的是对变量进行赋值,那么就会使用 LHS 查询;若是目的是获取变量的值,就会使用 RHS 查询es6

第二章: 词法的做用域

词法阶段

1.一个词法不可能同时在两个做用域中,做用域查找会在找到第一个匹配的标识符时中止
2.全局变量会自动成为全局对象的属性(据阮老师的博客上说这是因为js的设计这为了减小内存了留下的历史问题)
3.不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定。(词法做用域是静态做用域和动态的没有关系)github

欺骗词法做用域

1.eval函数:接受字符串代码他会在编译器执行在引擎快要执行的时候将这段代码写在他位于的位置,不推荐使用.不过能够解决var出现的变量死区的问题
2.with函数:简单来讲with函数{}之内的语句在当前的位置之内建立了一个做用域并且自动放入了吧obj对象当中的属性放了进去,这就有点想是在Chrome中的命令行写global.a = 0而后a=1进行赋值时同样的,依然不推荐使用web

//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;
}
//obj.a = 3
/***************我是分界线*****************/
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——很差,a 被泄漏到全局做用域上了!复制代码

这就是前面说的o2.a会进行LHS查询当查询到顶级时就会给全局变量赋值
eval和with我认为并非做为词法做用域的范围,由于词法做用域是在编译前就作好的,因此这个叫作欺骗词法做用域,固然由于是动态的因此会消耗性能编程

第三章:函数做用域和块做用域

函数中的做用域

先看下面代码函数bar(..) 拥有本身的做用域范围,全局做用域也有本身的做用域范围,它只包含了一个标识符:foo。而foo能够理解为一层楼进入一个房间的门,是一个入口.函数做用域
主要提供函数变量的访问,找不到一个变量就会去上一个做用域找,而以后所提到的原型链是一个对象的原型链是在这个对象的内部找属性找不到的时候就会去查找.(本身在看书的时候不当心弄混了)数组

function foo(a) { 
    var b = 2;
    // 一些代码
    function bar() { 
        // ...
    }
    var c = 3;
}复制代码

隐藏内部实现

在下面的代码当中doSomethingElse的调用并非最安全的,由于其余函数均可以调用安全

function doSomething(a) {
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
function doSomethingElse(a) { 
    return a - 1;
}
var b;
doSomething( 2 ); // 15复制代码

而下面的函数则是比较安全的bash

function doSomething(a) { 
    function doSomethingElse(a) {
        return a - 1; 
    }
    var b;
    b = a + doSomethingElse( a * 2 );
    console.log( b * 3 );
}
doSomething( 2 ); // 15复制代码

因此function(){}用来隐藏代码解决冲突(这是由于js在es5当中只有函数做用域并无块做用域)

函数做用域

js当中为了可以模仿块级做用域,边有人想到了用函数做用域模仿的概念.先来看看下面代码

//并不理想
function foo() { 
    var a = 3; 
    console.log( a ); 
} 
foo(); 
//方法1:
(function foo(){ 
    console.log( a ); // 3 
})();复制代码

虽然这种技术能够解决一些问题,可是它并不理想,由于会致使一些额外的问题。首先,必须声明一个具名函数 foo(),意味着 foo 这个名称自己“污染”了所在做用域(在这个 例子中是全局做用域)。其次,必须显式地经过函数名foo()调用这个函数才能运行其中的代码。然而使用了自执行函数之后欺骗编译器对于经过()或者+-*等等欺骗了编译器的检查(后面会提到)
因此忽略了function的声明语句.而这个语句的结果值就是这个函数调用之后就会执行

匿名函数和当即执行函数还有函数的声明和函数表达式

编写带有名字的函数便于理解

setTimeout( function timeoutHandler() { 
    // <-- 快看,我有名字了! 
    console.log( "I waited 1 second!" );
}, 1000 );复制代码

function 和 var 编译时存在函数的提高,也就是说var 和 function会优先的被编译器识别交给做用域。而后引擎在访问代码的时候就可以查询到编译器交过来的变量了
下面说的方法实际上是经过一些其余的表达式干扰编译器的判断,让编译器认为这并非一个声明,对于函数的表达式和函数的声明还有当即执行函数能够看看这两个博主的文章看看我还有我
1.编译器在编译代码的时候发现一行代码若是第一个出现的是funtion则会被理解为函数的声明语句(var也是而let的机制可能就不一样),编译器就会自动把它叫给做用域.函数的声明是不会有结果值的
2.当一个有function的函数的声明加入其余的东西时(例如括号+或者-等)编译器会把他认为是一个非声明的语句,而这些语句是须要引擎来执行的
3.当引擎执行代码的时候会发现这里面藏着一个非声明的语句因而就执行他这时候是有结果值的,因此能够对他进行调用

下面的代码就是例子(感觉一下js的黑暗吧)

//下面对能够对返回的结果值进行调用,括号的位置并不影响由于function(){}被做为表达式执行完之后会就会返回函数因此两个都行
( function foo(){} )()//ƒ foo(){}
( function foo(){} () )//ƒ foo(){}
//我本身又写了一下感觉邪恶吧
(function(){return (a)=>{ console.log(a); return (b)=>{console.log(b)}}})()(1)(2)
//下面没有返回的结果值就不能够因此报错
function foo(){}()
//因此你能够依靠this和做用域来实现let,若是没有声明就会出错
function foo(){console.log(a); (function(){this.a = 10})(); console.log(a); }
function foo(){console.log(a); let a = 10; console.log(a); }复制代码

块做用域

{}没法建立块做用域由于js并不支持块做用域,可是try{}catch{}却能够,和function(){
}我认为他们建立的实际上是函数做用域,其实他们一直是在用函数做用域模拟块做用域

第四章:提高

函数优先的原则

看下面的代码

foo(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};复制代码

上面的代码会执行1和下面的代码是等价的,这说明函数的声明是要比var提早的,我认为可能编译器在发现有function生命的时候会把var替换掉

function foo() { 
    console.log( 1 );
}
foo(); // 1
foo = function() { console.log( 2 );
};复制代码

第五章: 闭包

什么是闭包

其实我把闭包想象为一个被保存的做用域,而实现方式一般使用function(){}建立这样一个函数做用域的方式(固然也有其余的方式)

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。复制代码

看起来你并不以为这有什么牛逼的地方,可是其实js当中闭包是十分经常使用的功能(好比全部的回调函数其实都是闭包)

//当你把闭包的返回进入另外一个函数内部的时候,你就能够在另外一个函数内部访问他的变量!!!!!!!
function foo() { 
    var a=2;
    function baz() { 
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包!
}
//经过做用域访问的方式进行传递闭包
var fn; 
function foo() {
    var a=2;
    function baz() { 
        console.log( a );
    }   
    fn = baz; // 将 baz 分配给全局变量 
}
function bar() {
    fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2复制代码

做者也告诉咱们不只仅如此,闭包之所用重要是由于 在定时器、事件监听器、 Ajax请求、跨窗口通讯、Web Workers或者任何其余的异步(或者同步)任务中,只要使用了回调函数,本质都是在使用闭包!

function wait() {
    let a = 1;
    function test(){
        console.log(this.a)
        console.log(a)
    }
    return test;
}
var a = 2;
wait()();
// 定时器
function wait(message) {
    //这就是闭包
    function timer() {
        console.log( message );
    }
    //下面的就能够理解为引擎会在1s内调用一个函数,而这个函数就是闭包,他会访问message的做用域
    setTimeout( timer, 1000 ); 
}
wait( "Hello, closure!" );
//事件监听器
function setupBot(name, selector) {
$( selector ).click( function activator() {
console.log( "Activating: " + name ); });
}
     setupBot( "Closure Bot 1", "#bot_1" );
     setupBot( "Closure Bot 2", "#bot_2" );
//触发的activator函数也能够看作是一个闭包复制代码

循环和闭包

先看看代码

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

其实以上的输出结果并不会是1,2,3,4,5.反而回是6,6,6,6,6.这是由于setTimeout闭包并非当即执行的,而是延迟执行的.因此第一步会先把for循环走完,当延迟执行的函数从新回到这个做用域的时候,这里的变量已经面目全非了,因此为了可以维护闭包调用的做用域咱们会才去一些措施(我记得大搜车的笔试题就有这个)

//这样是不行的,虽然咱们确实建立了一个供闭包未来回头查看的做用域,可是这个做用域里面什么都没有
for (var i=1; i<=5; i++) { 
    (function() { 
        setTimeout( function timer() { console.log( i );}, i*1000 );})();
}
//因此像下面这样的才可以运行,由于这里面维护的做用域就再也不是空了,固然也是由于这里面是一个值变量
for (var i=1; i<=5; i++) { 
(function() {
    var j = i;
    setTimeout( function timer() {
                 console.log( j );
             }, j*1000 );
})(); }复制代码

模块

在前端方面最先的模块机制的实现其实就是闭包,开头我说一般使用function(){}来维持一个特定的做用域,而下面的返回object的对象将各个维持特定做用域的function(){}组合起来,也可以实现闭包.

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复制代码

首先,CoolModule() 只是一个函数,必需要经过调用它来建立一个模块实例。若是不执行外部函数,内部做用域和闭包都没法被建立。
其次,CoolModule()返回一个用对象字面量语法{ key: value, ... }来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。咱们须要保持内部数据变量是隐藏且私有的状态。能够将这个对象类型的返回值看做本质上是模块的公共 API。
从模块中返回一个实际的对象并非必须的,也能够直接返回一个内部函数。jQuery 就是一个很好的例子。jQuery 和 $ 标识符就是 juery 模块的公共 API但它们自己都是函数(使用jq的时候实际上是调用了他的构造函数创造了一个jq的节点)
这样就实现了访问API中的方法可是却又不会使变量污染,可是你必须使用它而后本身赋值一个变量
闭包的造成必须有两个条件:1.必须有像上面一CoolModule()同样的封闭函数,也就是闭包所能保留的做用域范围.2.封闭函数至少要返回一个函数去做为探测这个做用域的闭包

现代和将来的模块机制

看看下面代码

//这里的模块定义就像上面的那样返回的是由闭包函数组成的对象
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 
    }
})();
//首先定义一个本身的模块bar,用来分装一个说你好的方式
MyModules.define( "bar", [], function() { 
    function hello(who) {return "Let me introduce: " + who; }
    return {
    hello: hello
};});
//foo依赖于
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( "hippo" )); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO复制代码

实际上在foo中获得的闭包bar闭包和 var bar = MyModules.get( "bar" );获得的闭包是同样的

将来的模块机制

  1. 在es6中会把一个文件当作一个模块,我我的理解就是用一个(function 文件名(导入的其余文件){})将整个文件代码括起来
  2. es6的模块是比较稳定的,在以前的模块机制用函数来分装模块会致使只有在引擎执行代码的时候才会知道为何错,可是es6的模块机制会被编译器识别也就会在执行知道将会有什么错误.
  3. 这里对应的应该是require和import的区别由于在webstorm编写代码的时候,require即使是路径写的不对也不会在webstorm出现错误,可是使用import导入的时候若是不存在webstorm会提示你没办法导入,我想着就是webstorm后台为你编译进行提示错误吧
  4. 还有一点好处是若是b块里面用了c,当b导入到a中c也就会本身导入
  5. module和import的区别

    import 能够将一个模块中的一个或多个 API 导入到当前做用域中,并分别绑定在一个变量 上(在咱们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 咱们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操做能够在模块定义中根据须要使用任意屡次。

附录的内容

动态做用域

经过前面的学习咱们知道静态做用域也就是词法做用域,也就是词法做用域是由编译器提早执行代码的时候构造出来的一个做用域,我以为他必定是采用树进行存储的.而动态的做用域实际上更多的是指的this指针,也就是说在引擎执行代码的过程当中进行变化的.(大部分的做用域应该是词法做用域,可是不免的要使用一些在执行过程当中变化的做用域)

function foo(){
    console.log(a)//2
}
function bar(){
    var a = 3;
    foo(); 
}
var a = 2;
bar();复制代码

上面的代码当中foo()做用域中没有a变量,也就是说要执行RHS引用(固然也没有console变量他会执行LHS引用)因此会找到2

块做用域

以前说过js中是没有块级做用域的可是这其实并非一个正常的编程语言的行为,因此模拟块级做用域是很是重要的.其实在一些语法中就已经有了块级做用域
好比with 和 catch

try{throw 2;}catch(a){ 
    console.log( a ); // 2
}
console.log( a ); // ReferenceError复制代码

this词法

这里主要提到了箭头函数的用法,好比说

var obj = {
    id:"awesome",
    cool:function coolFn(){
        console.log(this.id);
    }
}
var id = "not awesome"
obj.cool();//"awesome"
setTimeout(obj.cool,100);//"not awesome"复制代码

obj.cool()当然是隐式绑定可是当放在函数当中的时候其实这个隐式绑定会被断开由于他把这个函数
的指针赋给了setTimeout的参数变量因此调用的时候实际上是cool()这种方式。除了文章中提到的self保存住this的方法,就是使用箭头函数的绑定能够写成这个样子

var obj = {
    count: 0,
    cool: function coolFn() {
        if (this.count < 1) {
            setTimeout( () => {  // 箭头函数是什么鬼东西?
            this.count++;
            console.log( "awesome?" );
            }, 100 );
        }
    }
};
obj.cool(); // "awesome?"复制代码

箭头函数的笔记在后面还会详细的学习记录下。

遗留问题

  1. 最后我仍是没有弄懂,附录中动态做用域的问题,做者也说了动态做用域关心的是这个调用的位置而不是声明的位置,因此若是按照做者的动态做用域的观点会输出this但是
    做者本身又否认了说this的实现原理并非一个纯粹的动态做用域。那他究竟是个什么?
    function foo(){
     console.log(a)//2
    }
    function bar(){
     var a = 3;
     foo(); 
    }
    var a = 2;
    bar();复制代码
相关文章
相关标签/搜索