2.1 语言按各种方法可以分为各种类型,按编译执行过程,可以分为编译型语言和解释型语言。
比如 c 语言,必须先经过编译生成目标文件,然后链接各个目标文件和库文件,生成可执行文件。
Java、scala 则是先编译成字节码,然后解释执行字节码(可以理解为编译型语言也可以理解为解释型语言)。
准确的理解,java 是编译型语言,源代码整个编译成字节码,java 字节码,是解释型语言。Python 是解释
型语言,不过也可以先进行编译,编译成 python 的字节码。
Javascript 是解释型语言。目前貌似还没有直接将 js 整个编译然后才执行(有说法是 js 动态性太强,先整体
编译难度太大,执行性能不如解释执行高)。
注意:解释型语言也是需要编译的。区分编译型语言和解释型语言,是看源代码是否整个编译成目标代码
然后执行还是编译一段执行一段。
JavaScript ( JS ) 是 一 种 轻 量 级 解 释 型 的 , 或 是 JIT 编 译 型 的 程 序 设 计 语 言 ( 参 考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript)
对于传统编译型语言来说,编译步骤分为:词法分析、语法分析、语义检查、代码优化和字节生成。
但对于解释型语言来说,通过词法分析和语法分析得到语法树后,就可以开始解释执行了(根据语法树和
符号表生成机器码)。
这也就解释了为什么都说 js 是解释执行的,读一句执行一句,但是实际上 js 中还没执行到的代码语法错误
导致整个 js 不会执行的问题。
在浏览器中,多个<script>标签中的 js 代码,是分段编译的,但是全局对象是共享的(某一个<script>标签中
的语法错误不会导致另一个<script>中的代码不执行)。这个可以参考
http://www.cnblogs.com/RunForLove/p/4629510.html
2.2 语言按变量的类型在编译时确定还是运行时确定分为静态语言和动态语言。
比如 java,String s = null;变量 s 的类型在编译时就可以确定为字符串类型。
比如 python,变量不需要声明,变量的类型在第一次赋值时由值的类型确定。
比如 js,var val;val = ‘1’;变量 val 在运行 val=’1’时才能确定为字符串类型。
js 是动态类型语言。
2.3 语言按变量的类型是否在运行时可以改变分为强类型语言和弱类型语言。
Java、scala 是强类型语言,变量一旦声明,它的类型以后不能被改变。
Python 是强类型语言。
Js 是弱类型语言。比如 var v = ‘1’;v=1;v=true;这在 js 中是合法的。
2.4 按语言范式可以分为声明式、命令式、函数式语言
声明式编程,告诉计算机我要做什么,而不是如何做。在更高层面写代码,更关心的是目标,而不是底层
算法实现的过程。 例如 css, 正则表达式,sql 语句,html, xml…
命令式编程,告诉计算机如何做,而不管我想要做什么。解决某一问题的具体算法实现。例如 java、c。
函数式编程,将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
很多语言并不是单纯的支持某一种范式,像 java8 也添加了部分对函数式的支持,js 是一个非常灵活的语言,
支持命令式和函数式编程。一般函数式语言都会有 filter、map、reduce 等函数。某些情况下用函数式编程
能更好的解决问题,所以对程序员的要求是不仅要熟悉命令式编程,还要熟悉函数式编程。
2.5 各种类型语言的优缺点
一般编译型语言性能比解释型语言高。但是由于编译型语言需要先进行编译。解释型语言的好处是,部署
到线上的是源代码,可以直接修改线上环境的代码,解决一些 bug。比如我们有时候直接修改线上的 js 代
码。编译型语言通常会用 xml 做配置文件,因为我们通常不会改编译后的字节码。解释型语言的配置,直
接写在源代码里更方便,用 xml 做配置就显得多余。
静态语言,有利于编译时检查。比如 java、在 ide 中为对象的一个不存在的属性赋值能在编译时检查出错误。
Js 是动态语言。对象的某个属性是否存在,在编译时无法确定。这导致某些错误要到运行时才可能发现。
所以一般 js 程序的正确性,更需要单元测试保证。
强类型语言由于类型在声明之后不允许改变,所以能实现编译时类型检查。
动态语言和弱类型语言,则更灵活,实现相同功能的代码量通常更少或者更容易实现复杂功能。当然可读
性可维护性方面不如静态语言和强类型语言。
三 单线程
js 代码的执行是单线程的。很多浏览器现在可以使用 worker 实现多线程。nodejs 环境也可以多线程执行 js。
单线程执行,避免了共享、锁、并发更新 dom 等非常棘手的问题。
如下代码
setTimeout(function(){
console.info(‘hello kitty’);
},1000);
通常认为会在 1 秒后在控制台打印 hello kitty。
但是 js 是单线程执行的,它并没有新开一个线程等到 1 秒后执行该线程。而是将回调函数放在 setTimeout
的回调队列里。即使 1 秒的时间到了,也要在执行完当前代码之后,才调用回调。
所以如果有
setTimeout(function(){
console.info(‘hello kitty’);
},1000);
While(true){
}
那么控制台是看不到 hello kitty 的。
结论是,setTimeout 的回调并不一定会准时执行,它可能会延迟,甚至不会执行。
这和 java 中新建一个 task 不一样。
四 prototype
与 java 是一个面向对象的语言不同,Js 是一个基于对象的语言。
也就是说,不要把 java 中的那一套拿过来学习 js,学习 js 要从 0 开始。
Js 中没有类的概念,没有继承,没有接口,没有多态,没有重载。
Js 和 java 是不同的编程语言,它有自己对世界的理解,有自己的抽象、模型、机制。
Prototype 就是 js 中实现复用的一种机制。
我们在代码中定义的每一个 js 对象,都有一个内部属性[[__proto__]],它的值是一个对象。
当我们访问对象的某个属性时,如果这个属性在该对象中未找到,那么解释器就会到该对象的[[__proto__]]
中去找,如果还没有找到,则会去[[__proto__]]的[[__proto__]]中找,直到找到 Object 的 prototype。这些
[[__proto__]]相连成为一个原型链。
例如:
var o2 = Object.create({a:'A'})
console.info(o2.a);//A
这里我们定义了一个对象 o2,指定它的[[__proto__]]为{a:’A’}。
通常,prototype 的使用并不是这样的,一般是在 prototype 中定义方法。
例如:
Var proto = {
Sleep:function(){
...
},
Eat:function(){
...
}
};
Var o1 = Object.create(proto);
o1.name = ‘lucy’;
Var o2 = Object.create(proto);
o2.nick = ‘luna’;
O1.sleep();
O2.sleep();
方法是各个对象公共的而属性则是各个对象自己的。
这里我们并没有解释什么是对象,也没有用到类的概念。
在 js 中,对象就是一组属性名及其对应的值的集合。简单理解就是键值对集合。
Js 对象的创建,并不是像 java 一样需要类。Js 中根本没有类的概念。
即使是新版的 js,提供了 class 语法,它实际上也只是个语法糖,和真正的面向对象中的类的概念是不同的。
和 prototype 强相关的还有函数。
Js 中的函数,被设计成为可以拥有多重身份的概念。后面会讲到。这里只讲它作为构造器时和 prototype 之
间的关系。
例如:
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype = {
Constructor:Person,
sayHello:function(){
Return “hello”;
}
};
Var p1 = new Person();
Var p2 = new Person();
在 js 中,每个函数都会有一个 prototype 属性,这个是我们可以访问的,也可以给它赋值。
每一个对象都有一个[[__proto__]]属性。这个[[__proto__]]和它的构造器的 prototype 指向的是同一个对象。
五 函数的多重身份
初学 js 的时候,非常容易被函数的多重身份弄晕。
一个 js 函数,可以作为对象、普通的函数、对象的方法、对象的构造器。
例如:
function foo(){
}
foo.a = ‘A’;
foo.b=’B’;
函数 foo 可以保存字符串 A 和字符串 B,它是一个普通对象,它的类型是 Function,就好比{}的类型是 Object。
function foo(){
console.info(‘hello kitty’);
}
foo();//hello kitty
这个时候,它是一个普通的函数。
Function Person(){
}
Person.prototype.sayHello = function(){
Console.info(‘hello’);
}
new Person().sayHello();
Person 是对象的构造器。sayHello 是对象的方法。
Js 解释器,将 function 作为函数和作为方法执行时,是不一样的。
主要是在作用域、this 方面。
六 词法作用域和动态作用域
作用域,准确的说是变量的作用域,它表示的是变量起作用的范围。
Js 中变量的作用域,是词法作用域,也叫静态作用域。
和词法作用域相对的,还有动态作用域。
一般我们接触的编程语言,都用词法作用域。比如 java、scala、python、js。
词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,去函数定义时的环境中查
询。
动态域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,到函数调用时的环境中查。
动态作用域由于变量的作用范围很难确定(如果变量既不是形参也不是函数内部定义的局部变量),很难
知道某个变量具体是指向哪个对象,所以现代编程都不用动态作用域。
七 可执行代码与执行环境
作用域在 ECMAScript5.1 规范中没有被专门解释,但是它解释了词法环境的概念,以及说明了函数的[[scope]]
属性。
网上博客以及一些书籍、都有介绍 js 的作用域的概念,但是往往只能解决一些简单的问题,对于更复杂的
问题,往往不能给予解释。讲的最合理深入的是《javascript 高级程序设计第三部》。为了做到对相关概念
及原理的清晰解释,这里用 ECMAScript5.1 规范中的术语。
7.1 核心概念
可执行代码、执行环境(及其三个组件词法环境、变量环境、this)、全局环境(是一个词法环境)、全局
对象 window、词法环境、环境记录项。
7.2 可执行代码
包括 3 种:全局代码、eval 代码、函数代码。
7.3 执行环境栈
当控制器转入 ECMA 脚本的可执行代码时,控制器会进入一个执行环境。当前活动的多个执行环境在逻辑
上形成一个栈结构。该逻辑栈的最顶层的执行环境称为当前运行的执行环境。任何时候,当控制器从当前
运行的执行环境相关的可执行代码转入与该执行环境无关的可执行代码时,会创建一个新的执行环境。新
建的这个执行环境会推入栈中,成为当前运行的执行环境。
7.4 执行环境的创建、入栈、出栈
解释执行 全局代码 或使用 eval 函数输入的代码会创建并进入一个新的执行环境。每次调用 ECMA 脚本
代码定义的函数也会建立并进入一个新的执行环境,即便函数是自身递归调用的。每一次 return 都会退出
一个执行环境。抛出异常也可退出一个或多个执行环境。
例如:
var val = 'hello';
function f1(){
function f2(){
}
f2();
}
function f3(){
}
f1();
f3();
首先在执行任何 js 代码之前,会创建一个执行环境,放入执行环境栈。
然后执行函数 f1,在执行 f1 中的所有代码之前,会创建一个执行环境,放入执行环境栈。
然后执行函数 f2,在执行 f2 中的所有代码之前,会创建一个执行环境,放入执行环境栈。
f2 执行完毕后,从执行环境栈弹出一个执行环境。
f1 执行完毕后,从执行环境栈弹出一个执行环境。
然后执行函数 f3,在执行 f3 中的所有代码之前,会创建一个执行环境,放入执行环境栈。
f3 执行完毕后,从执行环境栈弹出一个执行环境。
如图(执行 f2 时的执行环境栈)
其中执行环境的词法环境和变量环境组件始终为 词法环境 对象(这里要区分词法环境组件和词法环境对
象)。当创建一个执行环境时,其词法环境组件和变量环境组件最初是同一个值。在该执行环境相关联的
代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变。
变量对象保存该执行环境声明的变量和函数的引用(对于函数代码,还会有 arguments 和形参)。
如图:
7.5 词法环境
词法环境是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范
类型。一个词法环境由一个环境记录项和可能为空的外部词法环境引用构成。通常词法环境会与特定的
ECMAScript 代码诸如 FunctionDeclaration,WithStatement 或者 TryStatement 的 Catch 块这样的语
法结构相联系,且类似代码每次执行都会有一个新的语法环境被创建出来。
7.6 环境记录项
环境记录项记录了在它的关联词法环境域内创建的标识符绑定情形。
外部词法环境引用用于表示词法环境的逻辑嵌套关系模型。(内部)词法环境的外部引用是逻辑上包含内
部词法环境的词法环境。外部词法环境自然也可能有多个内部词法环境。例如,如果一个
FunctionDeclaration 包含两个嵌套的 FunctionDeclaration,那么每个内嵌函数的词法环境都是外部函数
本次执行所产生的词法环境。
共有 2 类环境记录项:声明式环境记录项 和 对象式环境记录项 。声明式环境记录项用于定义那些将 标
识符 与语言值直接绑定的 ECMA 脚本语法元素,例如 函数定义 , 变量定义 以及 Catch 语句。对象
式环境记录项用于定义那些将 标识符 与具体对象的属性绑定的 ECMA 脚本元素,例如 程序 以及 With
表达式 。
如下代码
其执行过程如下
上面的图漏了一个,环境记录项中还有一个属性 foo。
上面的图漏了一个,foo 对应的环境记录项中还有一个属性 bar。
上面的图,window 对象中漏了一个属性 foo,foo 对应的环境记录项中还有一个属性 bar。
这里只举了一个简单的例子。还有其他问题,比如词法环境组件和变量环境组件什么时候不一样?不一样
的时候分别又是什么?this 绑定什么时候不是绑定 window?除了全局执行环境,还有什么时候用到了对象
式环境记录项?涉及到构造器执行时,两个组件和 this 又是什么?
当使用 with 的时候, 会创建一个新的词法环境,该词法环境的环境记录项是一个对象式环境记录项 。然
后,用这个新的 词法环境 执行语句。最后,恢复到原来的 词法环境 。
这个时候,词法环境组件和变量环境组件便不一样了。
7.7 this 绑定
this 绑定,可以分为 4 种。默认绑定、隐式绑定、显示绑定、new 绑定。
也就是说代码中的 this 指代的是哪个对象,是不同的,它是根据调用方式和调用位置决定的。具体请参考
《你不知道的 javascript》上卷第二章。或者参考规范。
注意:全局执行环境的特殊性,全局执行环境的 this、词法环境组件的环境记录项、变量环境的环境记录
项,都是 window。而函数的执行环境不会出现这种情况。
所以在全局代码中有例如如下的特殊性:
var lang1 = 'javascript';
console.info(this.lang1);//javascript
this.lang2 = 'js';
console.info(lang2);//js
这在其他环境是模拟不出来的。你无法让一个函数的执行环境的词法环境组件的环境记录项和该执行环境
的 this 绑定到同一个对象。
由于 this 指向的对象并不是编译时确定的,它是运行时确定的,所以有的函数式编程经验的程序员强烈建
议 js 中尽量不要使用 this。
7.8 函数的创建和执行
如下代码
在执行全局代码时,会创建一个 foo 函数对象,设置它的各种内部属性,其中包括形参列表、原型、外部
词法环境(注意,这个时候函数 foo 以后执行时的作用域链除最后一个都确定了,这是在函数对象被创建
时确定的。foo 函数内部出现的标识符、要么在 foo 函数内部被声明、要么是 foo 函数的形参、否则从外部
词法环境中去找)等。
在执行 foo 函数时,会创建一个新的词法环境,用到它被创建时保存的外部词法环境,形成词法环境链(作
用域链)。
如果函数 foo 被多次调用,那么会多次创建 bar 函数对象。foo 执行完毕,bar 函数对象可能会被释放(如
果没有外部引用指向该函数对象,该函数对象将被释放)。
总之,就是理解函数的相关机制,要结合创建时和执行时。
7.9 闭包
闭包是函数式编程中的一个非常重要的概念。各种书籍对闭包的解释各不相同。
百度百科的描述是:闭包是指可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代
码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。
例如:
function intIterator(){
var i = 0;
return function iter(){
return i++;
}
}
var iter = intIterator();
console.info(iter());
console.info(iter());
console.info(iter());
console.info(iter());
console.info(iter());
打印结果 0,1,2,3,4
函数 iter 的代码中访问了一个自由变量 i,它在 iter 的形参和局部变量中并未定义。导致外部函数 intIterator
虽然执行完毕,但是局部变量 i 不会被释放。
结合前面执行环境的内容,具体过程如下:
执行 intIterator 函数之前
intIterator 函数返回之前
iter 函数返回之前
iter 函数返回之后
注意:即使 iter 只访问了 intIterator 中的一个变量 i,intIterator 对应的变量环境中所有的变量都不会被释
放!!!这就是闭包使用不当造成的内存溢出!!!
比如下面的代码
function intIterator() {
var i = 0;
var arr = [1,2,3,4,5];
return function iter() {
return i++;
}
}
var iter = intIterator();
变量 i 和数组 arr 就不会被释放,即使它在 intIterator 函数执行完毕之后。
对于 i,是我们以后用到的,但是对于 arr,以后再也没被用到。
八 异步、事件监听、回调、promise、deferred、generator、async
首先关于异步的概念,在编程语言里貌似没有精确的定义。或者是在多线程语言中定义的。要准确理解什
么是异步,建议参考 Linux 五种 IO 模型。
简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第
二段。
js 中异步实现的四种方式:回调函数、事件监听、发布订阅、promise。
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用
这个函数。
一般有的操作在时序上依赖于某个异步操作的结果。比如
$.get(url,data,function(res){
render(res);
...
},’json’);
反例:
var res;
$.get(url,data,function(r){
res = r;
},’json’);
render(res);
...
对于初学者,一定要强调正确写出包含异步操作的代码。特别是有多个异步的情况。
上面是回调函数的方式。回调函数,是在异步操作成功之后被系统自动调用的函数。我们并不需要手动调
用它。
事件监听的例子如下
$(‘#btn’).on(‘click’,function(){
});
我们会发现其实这里也有回调函数。其实$.get 用的 XMLHttp 也是用的事件监听机制,事件处理函数就是我
们写的回调。
在这里区别回调和事件监听,是因为某些情况如 setTimeout,并没有明显的体现出事件。
使用回调函数的写法,往往导致代码形式可读性差,难以维护。为了解决这个问题,jquery 提供了 Deferred
模块。当然还有其他的 js 库也有提供相关工具。
例子
var defer = $.post(url,data,null,’json’)
.done(function(res){
...
})
.fail(function(res){
...
});
这比回调好用多了,它能够实现回调不能实现的好多功能,比如发送请求的代码和回调函数的注册是分开
的、可以注册多个回调、可以把 Deferred 对象和其他异步的 Deferred 对象组合使用实现更复杂的逻辑等。
但是当有嵌套的异步时,代码还是很丑(丑表示可读性差,难以维护)。
ES6 中提供了 generator 函数解决这个问题。实际上是用协程解决该问题。
于是 ES7 中提供了关键字 async 和 await,它是 generator 的语法糖。
但是真心好用,异步代码写起来就像同步代码。可读性可维护性大大增强。
generator 我没用过,但是我写过很多 async 函数,感觉真的好用。
九 oo
oo 指的是面向对象。js 是一门基于对象的语言。
有些场景用面向对象思维解决问题比较方便,于是就有了 js 的面向对象。
js 中没有类的概念,但是和类作对比,构造器和类的相似性最大。构造器能够用来定义创建对象的模板。
我们可以把构造器当成类。但是由于语言机制,在接口、封装、继承、多态方面,和传统面向对象语言总
有一些差异。
没有实现封装的版本:
function Person(name,birthday){
this.name = name;
this.birthday = birthday;
}
Person.prototype = {
constructor:Person,
sayHello:function(){
return ‘hello’;
},
sleep:function(){
return ‘don’t bother me’;
}
};
var p = new Person(‘javascript’,’1995’);
p.sayHello();
p.sleep();
console.info(p.name);
这种方式是目前使用最广泛的方式,将一个类的属性定义和方法定义分开来写。
要实现封装,定义类时就需要做很多额外的工作,利用闭包,代码写起来会很多。
要实现继承,需要通过各种技术手段,解决各种问题。可以参考《javascript 高级程序设计》第六章。
多态包括方法重写和重载。你可以重写方法,但是你无法在不修改原来的方法的前提下实现方法重载。js
中的重载,是在同一个方法中手动对参数做判断。
总之,js 语言不是一门真正面向对象的语言,它有它自己的机制。不要强迫用传统面向
的思维和习惯使用它。
十 模块化
ES6 之前,要实现模块化,要么用第三方模块化工具,如 RequireJS 和 SeaJS,要么自己实现模块化工具。
nodejs 中用的是 CommonJS 规范。
ES6 添加了模块化特性。使用 import 和 export。但是我在 chrome 61.0.3128.0 上测试发现不行。所以为了兼
容性,要么用第三方库,要么用语法转换。
我曾经为了一个地图相关的项目,很多地图相关的 js 代码,但是没有用模块化,找个函数找半天,还不一
定找对。psi 产品中的 js 代码也没有模块化,有些函数、变量在当前并未定义,要确定这些函数、变量在哪
里定义,需要使用搜索,而不能根据当前文件的内容确定。
强烈推荐以后写项目的时候用模块化工具!!!
题外话:要关注 js 的新发展、一定要关注 MDN(Mozilla 开发者网络)。js 中非常多的新特性,都是从 Mozilla
发展而来的。很多问题在 MDN 上都可以找到。
十一 附录
11.1 书籍
《javascript 高级程序设计》
《javascript 核心指南》
《Ecmascript5.2 规范》
《你不知道的 javascript 上》
《你不知道的 javascript 中》
11.2 参考链接
JavaScript 技巧与高级特性
https://www.ibm.com/developerworks/cn/web/wa-lo-dojoajax1/
深入探讨 ECMAScript 规范第五版
https://www.ibm.com/developerworks/cn/web/1305_chengfu_ecmascript5/
ECMAScript 5.1 版本标准
http://lzw.me/pages/ecmascript/#229
Javascript 中的神器——Promise
http://www.jianshu.com/p/063f7e490e9a
谈谈 JavaScript 的词法环境和闭包(一)
https://segmentfault.com/a/1190000006719728
ES5 中新增的 Array 方法详细说明
http://www.zhangxinxu.com/wordpress/2013/04/es5%E6%96%B0%E5%A2%9E%E6%95%B0%E7%BB%84%E6%96
%B9%E6%B3%95/
MDN web 技术文档 javascript
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
11.3 感言
学习 js,一定要看书,看权威的书籍。Js 这门语言其实是初学者不容易掌握的,网上有的言论说 js 很容易
学,其实个人觉得 js 并不容易学。
该文档中的内容,难免由于个人知识有限,无法避免理解上的一些错误或者不全面,个人在学习 js 的时候,
尽量还是要看书,看规范。培训的内容,最好作为一个引子,引起大家的思考,使知道学习 js 有这些内容
需要掌握,有这些概念需要理清。