javascript高级培训

华苏javascript培训

@author:zhoujiaping
@date:2017-08-01

一 目的

使开发人员对javascript语言有较深入的理解,提升开发人员的js功底,在以后的项目中更准确、高效的使用js,编写出高质量的js程序。

二 解释型动态类型弱类型多范式语言

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。这在做系统集成时很方便。
编译型语言通常会用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时的执行环境栈)
这里写图片描述
每个执行环境包含三个组件

组件 作用目的
词法环境 指定一个词法环境对象,用于解析该执行环境内的代码创建的标识符引用。
变量环境 指定一个词法环境对象,其环境数据用于保存由该执行环境内的代码通过 变量表达式 和 函数表达式 创建的绑定。
This 绑定 指定该执行环境内的 ECMA 脚本代码中 this 关键字所关联的值。

其中执行环境的词法环境和变量环境组件始终为 词法环境 对象(这里要区分词法环境组件和词法环境对象)。当创建一个执行环境时,其词法环境组件和变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变。
变量对象保存该执行环境声明的变量和函数的引用(对于函数代码,还会有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有这些内容需要掌握,有这些概念需要理清。
由于内容较多,时间和精力有限,不能对每个主题作深入的讲解,有些内容甚至没有提及,例如立即执行函数、函数的定义方式、构造器的执行过程等,这些内容需要同学们自己去学习。

猜你喜欢

转载自blog.csdn.net/zhoujiaping123/article/details/79564131