你不知道的JavaScript-上卷 读书笔记-part1

书评

豆瓣
这本书很适合初级前端开发者上升至中级前端开发者,很好的阐述了JavaScript的闭包,原型,类,编译,赋值的问题.而且这还是上卷,还会有中卷,下卷,等等之类的.我会从这本书里选取一些比较重要的内容放在这篇文章当中(实际上这本书全部内容都重要). let’s do it

作用域

编译器原理简释

var a=2
当我们看到var a=2的时候引擎和编译器会做什么呢?

  1. 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的合集中.如果是.编译器会忽略该声明,继续进行编译.否则它会要求作用域在当前的作用域合集中声明一个新的变量,并且命名为a.
  2. 接下来编译器会为这个引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作.引擎运行时会首先询问作用域,在当前的作用域合集中是否存在一个叫a的变量,如果否,引擎就会使用这个变量;如果不是,引擎就会继续查找该变量.

引擎与作用域的对话

RHS引用是找到这个变量所在的地址,但是不赋值 赋值是等号做的事情
LHS引用是赋值时把RHS找到的地址赋值给LHS

1
2
3
4
function foo(a){
console.log(a);//2
}
foo(2)

把这段代码想象成一段对话是这样的

  • 引擎:我说作用域,我需要为foo进行RHS引用,你见过他吗?
  • 作用域:别说,我还真见过,编译器那小子刚刚声明了它.它是一个函数,给你.
  • 引擎: 哥们太够意思了!好吧,我来执行以下foo.
  • 引擎:作用域还有个事,我需要为a(函数传参中的a)进行LHS引用,这个你见过吗?
  • 作用域:这个也见过,编译器最近把它声明为foo的一个形式参数,拿去吧.
  • 引擎:大恩不言谢,你总是这么棒,现在我要把2赋值给a.
  • 引擎:哥们,不好意思又来打扰你.我需要为console进行RHS引用.你见过它吗?
  • 作用域:咋们谁跟谁啊.console是个内置对象,给你
  • 引擎:么么哒,我的看看这里面是不是有个log(…).太好了,找到了,是一个函数.
  • 引擎:哥们,能再帮我找一下对a的RHS引用吗?虽然我记得它,但想再确认一次.
  • 作用域:放心吧.这个变量也没有变动过.拿走.不谢.
  • 引擎:真棒,我来把a的值,也就是2.传递进log(…)

作用域嵌套

1
2
3
4
5
function foo(a){
console.log(a+b)
}
var b=2;
foo(2);

首先在浏览器中最顶端的作用域就是window也就是全局作用域.那么上述代码在全局作用域中创建了一个作用域叫foo.当引擎去解析执行的时候.对b进行RHS引用的时候在当前foo作用域是找不到的.于是去foo的上级作用域即全局作用域去查找b这个变量.

异常

1
2
3
4
5
function foo(a){
console.log(a+b);
b=a;
}
foo(2)

如果RHS遍历了所有的嵌套作用域都找不到该变量,引擎就会抛出ReferenceError异常.
如果执行的是LHS查询时.如果在顶层(全局作用域)中也无法找到目标变量,那么全局作用域中就会创建一个具有该名称的变量.并且将其返回给引擎
ReferenceError异常的意思是作用域判别失败相关.
TypeError则代表作用域判别成功,但是对结果的操作是非法或不合理的.

词法作用域

词法阶段

词法作用域也叫静态作用域.其作用域只在引擎初始化的时候就已经定好了.不会跟随代码的执行而动态改变作用域

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

这里面有三个嵌套的作用域 这里来分析一下

  • window(全局作用域)
  • window=>foo
  • window=>foo=>bar

作用域是嵌套的,上面也说了当编译器在当前作用域找不到的时候会在当前作用域创建一个变量.赋值的时候则会逐级递归查找当前作用域是否存在当前变量.不存在则会创建全局变量.
因为作用域是嵌套的.嵌套中的作用域可以访问上层作用域的值.所以在bar这个函数里,并没有a变量.但是它会从它上层作用域foo去查找.
全局变量自动会成为window(浏览器)的属性.比如上述的foo(2)可以用window.foo(2) 来写
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
词法作用域只会查找一级标识符.比如上述的foo.bar.baz,词法作用域查找只会试图查找foo标识符.找到这个变量后.对象属性访问规则会分别接管对bar和baz属性的访问.

欺骗词法

欺骗词法作用域会导致性能下降

eval

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

eval动态在foo作用域中创建了一个b变量,并且遮蔽掉了外部(window)中的b变量

with

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

1
2
3
4
5
6
7
8
var obj={
a:1,
b:2,
}
with(obj){
a=3;
b=4;
}

但实际上这不仅仅是为了方便访问对象属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 泄露到全局作用域中了

之所以o1能够正确被赋值是因为o1存在a这个属性.o2不存在a这个属性.所以with中引擎找不到这个属性的时候则会使用正常的LHS引用.所以泄露到全局中了

函数作用域与块状作用域

这一节主要讲述的是如何利用匿名函数来创建作用域,这样就不会污染全局命名空间了
匿名函数是可以具名的,并且效果一样

1
2
3
setTimeout(function timeoutHandler(){
console.log("i waited 1 second")
},1000)

如果用匿名函数在运行的过程中报错了那么浏览器只会返回一个anonymous function 如果具名的话那么会返回当前那个函数的名称

1
2
3
4
5
6
7
(function(){throw new Error("")})()
//结果
(anonymous function) @ VM419:2
(anonymous function) @ VM419:2
InjectedScript._evaluateOn @ VM410:904
InjectedScript._evaluateAndWrap @ VM410:837
InjectedScript.evaluate @ VM410:693

JavaScript的块级作用域有三个 一个是ifelse创建的 一个是try catch 还有一个是with 用with从对象创建出的作用域仅在with声明中而非外部作用域中有效

提升

JavaScript有两种提升 一种是var xxx;一种是函数提升 function xxx(){}
其中函数提升的优先级要大于var

1
2
3
4
5
6
7
8
foo();//1
var foo;
function foo(){
console.log(1);
}
foo=function(){
console.log(2);
}

编译器首先会提升function声明至作用域顶端 然后再运行foo()
并且函数提升无法被if else所控制

1
2
3
4
5
6
7
foo();//a
var a=true
if(a){
function foo(){console.log("a")}
}else{
function foo(){console.log("b")}
}

无论作用域的声明出现在什么地方,都将在代码本身被执行前首先进行处理.可以将这个过程形象地想象成所有的声明(变量和函数)都会被”移动”到各自作用域的最顶端.这个过程被称为提升

作用域闭包

具体的闭包这里不再做笔记了,这里只做如何用闭包来实现module(模块)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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!

这个模式在JavaScript称为模块,上述的返回值可以看成模块的公共API.
实际上从模块返回一个实际的对象并不是必须的,也可以直接返回一个内部函数.jquery和$就是一个很好的例子.jquery与$就是jquery模块的公共API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)

模块模式需要具备两个必须条件

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例).
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包.并且可以访问或者修改私有的状态.

一个具有函数属性的对象本身并不是真正的模块,从方便观察的角度看.一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块.
模块也可以轻而易举的实现单例模式 只需要改成IIFE(匿名函数立即执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo=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
}
}()
foo.doSomething()//cool
foo.doAnother();//1!2!3!

模块模式另一个简单但强大的变化用法是.命名将要作为公共API返回的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var foo=(function CoolModule(id){
function change(){
//修改公共API
publicAPI.identify=identify2;
};
function identifty1(){
console.log(id)
};
function identifty2(){
console.log(id.toUpperCase());
};
var publicAPI={
change:change,
identifty:identifty1
}
})("foo module");
foo.identifty();//foo module
foo.change();
foo.identifty();//FOO MODULE

通过在模块实例内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或者删除方法和属性已经修改他们的值

现代的模块机制

这里简略实现下模块引入机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var MyModule=(function Manager(){
var modules={};
//name 定义的模块名字
//deps 需要加载引入的模块名字
//impl 新的模块函数
function define(name,deps,impl){
for(var i=0;i<deps.length;i++){
deps[i]=modules[deps[i]]
}
//存储模块 执行impl同时传递deps的返回值
modules[name]=impl.apply(impl,deps)
}
function get(name){
return mdoules[name]
}
return{
define:define,
get:get
}
})()

使用就不演示了,比较简单的一段代码.
模块有两个主要特征

  • 为创建内部作用域而调用了一个包装函数;
  • 包装函数的返回值必须至少包括一个内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包

this

首先需要明白 this并不是总是指向函数本身.this在任何情况下都不指向函数的词法作用域.this指向的是对象.在JavaScript内部,作用域的确和对象相似.可见的标识符都是它的数学.但是作用域”对象”无法通过JavaScript代码访问,它存在于JavaScript引擎内部

this到底是什么

this是运行时进行绑定的.并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件.this的绑定和函数声明的位置没有任何关系.只取决于函数的调用方式.
当一个函数被调用时,会创建一个活动记录(上下文),这个记录会包含函数在哪里被调用(调用栈),函数的调用方法,传入的参数等信息,this就是记录其中的一个属性,会在函数执行的过程中用到

this的调用位置

调用位置就是函数在代码中被调用的位置而不是声明的位置.
最重要的是分析调用栈(就是为了到达当前执行位置所调用的所有函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function baz(){
//调用栈:baz
//调用位置 全局作用域
console.log("baz");
bar();// bar的调用位置
}
function bar(){
// 调用栈 baz->bar
// 调用位置在bar中
console.log("bar");
foo()// foo的调用位置
}
function foo(){
// 调用栈 bz->bar->foo
// 调用位置 bar
console.log("foo");
}
baz();// baz的调用位置

this的默认绑定

this默认是绑定在window下的

1
2
3
4
5
function foo(){
console.log(this.a)
}
var a=2;
foo();//2

this的隐式绑定

对象属性引用链中最有最顶层或者最后一层会影响调用位置

1
2
3
4
5
6
7
8
9
10
11
12
function foo(){
console.log(this.a);
}
var obj2={
a:42,
foo:foo
}
var obj1={
a:2,
obj2:obj2
}
obj1.obj2.foo();//42

隐式丢失

被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定.从而把this绑定到全局对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a)
}
function doFoo(fn){
// fn其实是引用的foo
fn();//调用位置
}
var obj={
a:2,
foo:foo
}
var a="oops,global";
doFoo(obj.foo);//"oops,global"

this的显式绑定

利用call和apply来修复this绑定对象

硬绑定

1
2
3
4
5
6
7
8
9
10
11
12
function foo(){
console.log(this.a)
}
var obj={
a:2
}
var bar=function(){
foo.call(obj)
}
bar();//2
//硬绑定的bar不可能再修改它的this
bar.call(window);//2

上述是封装了一层也就是在bar内强制性的绑定了一个对象 所以外界怎么修改bar的调用位置都不可能印象到foo函数
另外ES5的bind就是一种硬绑定

new绑定

使用new来调用函数,或者发生构造函数调用时,会自动执行下面的操作

  1. 创建(或者说构造)一个全新的对象.
  2. 这个新对象会被执行[[原型]]连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1
2
3
4
5
function foo(a){
this.a=a;
}
var bar=new foo(2);
console.log(bar.a);//2

优先级

1.函数是否在new中调用(new 绑定)?如果是的话this绑定的是新创建的对象

1
var var = new foo()

  1. 函数是否通过call,apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定对象

    1
    var bar = foo.call(obj2)
  2. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象

    1
    var bar=obj1.foo()
  3. 如果都不是的话,那么使用默认绑定.非严格模式下绑定到全局

    1
    var bar=foo()

如果call,apply传递的是null那么实际上应用的是默认绑定规则

间接引用

间接引用最容易发生在赋值的时候

1
2
3
4
5
6
7
8
function foo(){
console.log(this.a)
}
var a=2;
var o={a:3,foo:foo};
var p={a:4};
o.foo();//3
(p.foo=o.foo)();//2

赋值表达式p.foo=o.foo的返回值是目标函数的引用,因此调用位置是foo而不是p.foo()或者o.foo().根据我们之前说过的这里会引用默认绑定

软绑定

硬绑定是把this强制性绑定到指定的对象(除了new),问题在于硬绑定会大大降低函数的灵活性.使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(!Function.prototype.softBind){
Function.prototype.softBind=function(obj){
var fn=this;//当前函数
var curried=[].slice.call(arguments,1);
var bound=function(){
//如果不存在this或者默认的this指向全局 那么则动态设置this为传递进来的obj否则就默认绑定的this
return fn.apply((!this||this===(window||global))?obj:this,curried.concate.apply(curried,arguments))
}
//继承fn
bound.prototype.Object.create(fn.prototype)
return bound
}
}

下面来看看softBind是否实现了软绑定功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo(){
console.log("name"+this.name);
}
var obj={name:"obj1"},
obj2={name:"obj2"},
obj3={name:"obj3"};
var fooObj=foo.softBind(obj);
fooObj();//name:obj
obj2.foo=foo.softBind(obj);
obj2.foo();//name:obj2 //这里的this不等于window 所以绑定的是调用者也就是obj2
fooObj.call(obj3);//name:obj3 //这里的this等于window 所以应用的是obj
setTimeout(obj2.foo,10);//name:obj 这里的this为window但是因为没有执行这个函数 所有这里引用的是obj 这里的代码可以这样分析
obj2.foo=function(){
return fn.apply((!this||this===(window||global))?obj:this,curried.concate.apply(curried,arguments))
}

this词法

1
2
3
4
5
6
7
8
9
10
function foo(){
var self=this;
setTimeout(function(){
console.log(self.a)
},1000)
}
var obj={
a:2
}
foo.call(obj);//2

如果你经常编写this风格的代码,但是绝大部分都会至用self=this来否定this的机制,那你或许应该

  1. 只使用词法作用域并完全抛弃错误this风格的代码(如module模式)
  2. 完成采用this风格,在必要时使用bind(…),尽量避免使用self=this