Javascript简介

Javascript的特性

与其他语言的不同之处:

  1. 在JavaScript 中,函数与其他对象共存,并且能够像任何其他对象一样地使用。函数可以通过字面量创建,可以赋值给变量,可以作为函数参数进行传递,甚至可以作为返回值从函数中返回。
  2. 函数闭包
  3. JavaScript 还没有(类似C语言中的)块级作用域下的变量,取而代之则只能依赖函数级别的变量和全局变量.
  4. 不同于其他主流的面向对象语言(例如C#、Java、Ruby)使用基于类的面向对象,JavaScript 使用基于原型的面向对象

特殊的功能, 特性:

  1. 生成器, 一种可以基于一次请求生成多次值的函数,在不同请求之间也能挂起执行.
  2. Promise,让我们更好地控制异步代码。
  3. 代理,让我们控制对特定对象的访问。
  4. 高级数组方法,书写更优雅的数组处理函数。
  5. Map,用于创建字典集合;Set,处理仅包含不重复项目的集合。
  6. 正则表达式,简化用代码书写起来很复杂的逻辑。
  7. 模块,把代码划分为较小的可以自包含的片段,使项目更易于管理。

转换编译器:

  • 当新的标准制定, 新的特性出现时, 部分用户往往仍然使用老旧的浏览器, 一种解决方法是使用转换编译器, 将较新的Js代码转化为等价的, 能在当前浏览器运行的代码.
  • Traceur 和 Babel是较为流行的两种

理解浏览器

浏览器环境概念:

  1. 文档对象模型(DOM): Web应用的结构化的UI表现形式, 最初由web应用的HTML代码构成;
  2. 事件: 大部分JavaScript 应用都是事件驱动的应用,这表示大部分代码执行在对某个特殊事件响应的上下文中。
    1. 如网络事件、计时器、用户生成事件例如点击、鼠标移动、键盘按压事件等。
  3. 浏览器 API: 获取设备的信息、存储本地数据或与远程浏览器交互的API。

调试工具: 探索 DOM、调试 JavaScript、编辑 CSS 样式和跟踪网络事件等。

测试: assert(condition, message); 第一个参数是一个应为真值的条件,第二个参数是当断言为假时所展示的一句话。

性能分析: 把要被测量的代码放在两个计时器调用之间;

1
2
3
4
5
console.time("My operation"); //My operation是名字

/* codes to count*/

console.timeEnd("My operation");

跨平台开发: 通过使用浏览器和 Node.js(源自于浏览器的环境),你能够开发几乎你能想到的任何类型的应用。

  • 桌面应用,通过使用如NW.jsElectron的库可以开发桌面应用。
    • 包装javascript和浏览器核心
  • 移动应用,使用类似Apache Cordova的框架开发。
  • 使用Node.js 开发服务器端应用和嵌入式应用,Node.js 是源自于浏览器的环境,使用了很多类似浏览器的底层原理。

浏览器页面构造过程

FIG 1

Fig 1.1 web应用的生命周期

主要由两个步骤构成: 页面构建和事件处理;

页面构建

又分为两个步骤, 在页面构建过程中交替进行;

  1. 解析HTML代码并构建文档对象模型(DOM);
  2. 执行 JavaScript 代码. 当遇到脚本节点时执行;

HTML 解析和 DOM 构建

尽管 DOM 是根据 HTML 来创建的,两者紧密联系,但需要强调的是,它们两者并不相同。你可以把 HTML 代码看作浏览器页面 UI 构建初始DOM 的蓝图。

浏览器修正了错误的HTML代码

当页面构建遇到脚本元素时, 会暂停构建DOM转而执行JavaScript代码;

DOM与脚本的关系:

  1. window 对象是获取所有其他全局对象、全局变量(甚至包含用户定义对象)和浏览器 API 的访问途径。
  2. 全局 window 对象最重要的属性是 document,它代表了当前页面的 DOM。
    • 通过使用这个对象,JavaScript 代码就能在任何程度上改变 DOM

全局代码与函数代码:

  1. 函数代码指的是包含在函数中的代码,全局代码指的是位于函数之外的代码;
  2. 全局代码以一种直接的方式自动执行,每当遇到这样的代码就一行接一行地执行。
  3. 函数代码必须被调用才执行;

事件处理

浏览器处理代码特性:

  • 浏览器同一时刻只能执行一个代码片段,即所谓的单线程执行模型。
  • 所有生成的事件都被放在同一个事件队列中(注册事件监听器), 从头部开始被处理;

事件类型:

  • 浏览器事件,例如当页面加载完成后或无法加载时;
  • 网络事件,例如来自服务器的响应(Ajax 事件和服务器端事件);
  • 用户事件,例如鼠标单击、鼠标移动和键盘事件;
  • 计时器事件,当timeout 时间到期或又触发了一次时间间隔。

注册事件监听器方式:

  • 通过把函数赋给某个特殊属性;
    • window.onload = function(){};, 将函数赋值给window对象的onload属性;
    • 这一方式的缺陷在于对于一个事件只能注册一个事件处理器, 创建新的处理器时会将上一个给改写掉;
  • 通过使用内置addEventListener方法。
1
2
3
4
5
6
7
8
9
document.body.addEventListener("mousemove", function() {	//#为mousemove事件注册处理器 
var second = document.getElementById("second");
addMessage(second, "Event: mousemove");
});

document.body.addEventListener("click", function(){ //#为 click 事件注册处理器
var second = document.getElementById("second");
addMessage(second, "Event: click");
});

函数

函数与对象

函数中最重要的概念: 函数是第一类对(first-class objects),可以被视为其他任意类型的 JavaScript 对象。

  • 能被变量引用:
  • 能以字面量形式声明:
    • function ninjaFunction(){}
    • var ninja = {};
  • 甚至能被作为函数参数进行传递。
    • call(function(){})

回调函数(callback): 将在随后调用的函数, 也即作为参数被其它函数执行的函数;

执行 useless(getText)调用后的执行流

回调函数排序:我们提供一个函数用于比较, 返回值大于0需要调换,小于等于0不需要;在比较时调用回调来决定数组的顺序;

1
2
3
4
var values = [0,1,2,9,6,5,3,4]
values.sort(function(value1,value2){
return value1-value2
})

储存函数: 存储元素唯一的函数集合;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var store = {
nextId: 1,
cache: {},//使用一个对象作为缓存,我们可以在其中存储函数
add: function(fn) {
if (!fn.id) {
fn.id = this.nextId++;
this.cache[fn.id] = fn;//仅当函数唯一时,将该函数加入缓存
return true;
}
}
};

function ninja(){}
//测试上面代码是否按预期工作
assert(store.add(ninja),
"Function was safely added.");
assert(!store.add(ninja),
"But it was only added once.");

自记忆函数: 当函数计算得到结果时就将该结果按照参数存储起来,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function isPrime(value) {
if (!isPrime.answers) {
isPrime.answers = {}; //创建缓存
}

if (isPrime.answers[value] !== undefined) {//检查缓存的值
return isPrime.answers[value];
}

var prime = value !== 1;

for (var i = 2; i < value; i++) {
if (value % i === 0) {
prime = false;
break;
}
}

return isPrime.answers[value] = prime;//存储计算的值
}

assert(isPrime(5), "5 is prime!" );
assert(isPrime.answers[5], "The answer was cached!" );//测试该函数是否正常工作

函数定义方式

  • 函数声明: function myFun() { return 1;}
    • 函数声明与函数表达式的不同之处
    • 函数声明:作为独立表达式;函数表达式:作为其他语句的部分,作为右值/参数/返回值;
    • 对于表达式, 函数名不是必须的,对于声明, 他们被引用的唯一方式是通过名字;
    • 立即调用函数表达式IIFE):(function(){})(2),创建了一个新函数并调用;
      • 括号的作用: 不加括号时, 以function开头的语句会被解释为声明, 然而没有函数名, 故而会报错
      • 上图四个语句都是立即函数,但是使用一元操作符指明处理的是表达式,而非语句;符号得到结果没有被储存,关键在于IIFE被调用了;
  • 箭头函数(lambda函数): param => expression
    • 省去function,大括号,return;
    • param: 参数, 单个参数省略括号, 多个参数与声明一致;
    • expression: 多行表达式需要{};
  • 函数构造函数: 以字符串形式构造函数;new Function('a', 'b', 'return a + b')
  • 生成器函数:在执行过程中,能够退出这个函数再重新进入,过程中保留函数内变量值;function* myGen(){ yield 1; }

函数参数

  • 参数性质:
    • 实参多于形参: 按照顺序赋值, 多余的实参不会被赋值;
    • 形参多于实参: 没有对应实参的形参则会被设为undefined;
  • 剩余参数:
    • 剩余参数以……做前缀, 且只能是最后一个参数, 被放到以去除…后的名称(reaminingNumbers)的数组中;
  • 默认参数:
    • 原理是未被赋值的形参为undefined
    • 另一种方法是函数重载: 定义一个名字相同但参数不同的函数;但JavaScript不支持;
    • 直接在定义中为参数赋默认值
    • 每次函数调用时都会从左到右求得参数的值,当对后面的默认参数赋值时可以引用前面的默认参数

函数调用

  • arguments参数: 传递给函数的所有参数的集合;
    • 为实参的集合, 不论是否有对应形参;
    • 通过数组下标方式访问参数, arguments[i];
    • arguments.length获取实参个数,但它不是数组,只是与数组类似,在其上使用数组的方法会报错;
      • 相比较之下, 剩余参数则是作为数组;
    • arguments对象是函数参数的别名, 在函数内改变arguments对象的值也会改变对应形参,反之亦然;
      • 在JavaScript 提供的严格模式(strict mode)中无法再使用别名。"use strict";
  • this参数:函数调用相关联的对象(函数上下文)
    • this 参数的指向不仅是由定义函数的方式和位置决定的,同时还严重受到函数调用方式的影响;
    • 函数调用的四种方式:
      • 作为一个函数(function)直接被调用;test()
        • 非严格模式下,this==window全局对象;严格模式下this==undefined;
      • 作为一个方法(method),关联在一个对象上,实现面向对象编程;myobj.test(), 此时this指向该对象;
      • 作为一个构造函数(constructor),实例化一个新的对象;new ObjName()
          • 当使用关键字 new 调用函数时,会创建一个空的对象实例并将其设置构造函数的上下文
          • 当构造函数有非对象返回值时,用new调用则返回新建对象,直接调用则返回该值;
          • 但当返回对象时, this对象将被舍弃;
        • 构造函数命名通常以大写字母开头,为描述对象的名词;函数方法则以小写字母开头,为描述行为的动词;
      • 通过函数的apply 或者call 方法;obj.apply(...) or obj.call(...)
          • Button函数中, 原本通过button.click调用this应该指向button,但是由于我们将其绑定到了按钮上,故而this指向了elem元素;
          • apply方法, 上下文对象和参数数组;
          • call方法, 上下文对象和参数, 无需使用数组传递参数;
        • forEach方法(call|apply)迭代数组
      • 解决上下文问题的其他方法:
        • 箭头函数:
          • 箭头函数从定义时的所在函数继承上下文,相比较函数表达式指向全局对象;
          • 与清单4.10比较:4.10中Button构造函数的click函数上下文被addEventListener绑定到了elem元素, 而箭头函数的click函数从Button函数处继承上下文,故仍然指向button.
          • 存在的问题:
            • click 箭头函数是作为对象字面量的属性定义的,对象字面量在全局代码中定义, 所以箭头函数this指向window;
        • bind方法:
          • 不管如何调用该函数,this 均被设置为对象本身。
          • 被绑定的函数与原始函数行为一致,函数体一致。

闭包与作用域

  • 闭包能够允许函数访问并操作函数外部变量;
    • 全局作用域实质上是一种闭包, 但是从未消失;
    • 通过outerFunction我们封装了一个innerFunction,并将该function赋给全局变量later,从而能够访问到inner*,并且该函数的作用域为全局+outerFunction;
      • 这里ninja是outerfunction的局部变量,按理来说应当无法访问,但是声明inner*时,创建了一个闭包,不仅包含了函数的声明,还包含了在函数声明时该作用域中的所有变量。
      • 也即是说,创建闭包不仅保存了函数,还有其作用域内的变量;
      • feints变量不是通过this.feints方式定义的,故而不能直接访问,但是其包含于Ninja的作用域中,能被this.feint()函数所访问;
      • 这一功能有点类似于shiny的模块化,闭包被不同的参数调用,其内部变量互不影响;
  • 全局执行上下文只有一个,当JavaScript程序开始执行时就已经创建了全局上下文;而函数执行上下文是在每次调用函数时,就会创建一个新的。

    • 函数上下文是内部的, 而执行上下文是JS引擎追踪函数执行使用的;
      • 基本和其他语言差异不大;
  • 词法环境: 也即作用域(scopes);
    • 当使用变量时, 从内向外开始查找, 从调用栈从上往下一级一级查, 直至找到或者是全局作用域中都没有而报错;
      • 不是从定义函数的环境查找;
  • 变量类型:
    • const关键字: 声明的变量的值无法变更(指用新的值覆盖);compared with var and let(能多次覆盖)
      • 用于定义无需重新赋值的变量, 或者是某个固定的值(通常用于描述性变量名替代单纯数值);
        • 不允许将全新的值赋值给const变量,但是可以修改;
    • var关键字:声明变量是在距离最近的函数内部或是在全局词法环境中定义的;
        • 与C不同,js不关注块级作用域,var声明的变量在距离最近的函数或全局作用域中实现;
        • forLoop的块级作用域中声明的元素仍能被外部访问;
      • 与var不同, let和const在最近词法环境定义变量(块级,循环,函数,全局);
        • 块级作用域: for(){}, if(){}, with(obj){}, try{}/catch{},even simple {};
    • 标识符注册:
        • 定义在使用之后(谈恋爱要在世界拯救之后??);
      • 注册流程:
        1. 找到函数声明, 创建arguments和函数参数;
        2. 扫描当前代码进行函数声明(不会扫描其他函数的函数体),对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上.
        3. 扫描当前代码进行变量声明。
          1. 在函数或全局环境中,查找所有当前函数以及其他函数之外通过 var 声明的变量,并查找所有通过 let 或 const 定义的变量。
          2. 在块级环境中,仅查找当前块中通过 let 或 const 定义的变量。

generator和promise

生成器函数

  • 基于每次请求生成值, 从而生成一组序列;
    • 每次请求生成新的值 / 或者告诉我们不再生成新值;
      • 在关键字function前加星号*,从而在生成器内部使用yield生成独立值;
      • for-of循环: 新的循环方式;
      • 将值赋给const变量得到迭代器object;
      • iter.next()返回一个对象,包含
        • result.value: 返回的值,如生成已结束则为undefined;
        • result.done: 是否生成器结束,如已结束则为true;
        • 调用next方法 -> 执行代码直到遇到yield -> 返回中间值;
        • yield* otherGenerator将执行权交给另外的生成器;
        • 整体执行逻辑不变,仍旧是遇到yield就返回值, 相当于生成了一个栈, 新加一个生成器就加一层栈;
  • 生成器用法:
    • 生成ID序列:定义一个无限循环的生成器,每次返回++ID;
    • 遍历DOM树:
      • 深度优先,优先往下访问;
  • 向生成器发送值:
      • 情况1:在初始状态调用并传入参数;
      • 情况2:next方法传入参数;
      • 用throw方法向迭代函数抛出一个错误,该错误会被catch()函数获取,传递给参数e;
  • 生成器执行流程:
      • 生成器比较特殊,它不会执行任何函数代码。而是生成一个新的迭代器再从中返回,通过在 代码中用 ninjaIterator 可以来引用这个迭代器。
      • 由于迭代器是用来控制生成器的执行的,故而迭代器中保存着一个在它创建位置处的执行上下文。
      • 每次调用next方法,不是像普通函数那样,生成新的上下文,而是把原有的上下文重新放入栈中;
    • 挂起开始 — 创建了一个生成器后,它最先以这种状态开始。其中的任何代码都未执行。
    • 执行 — 生成器中的代码已执行。执行要么是刚开始,要么是从上次挂起的时候继续的。
      • 当生成器对应的迭代器调用了next方法,并且当前存在可执行的代码时,生成器都会转移到这个状态。
    • 挂起让渡 — 当生成器在执行过程中遇到了一个yield表达式,它会创建一个包含着返回值的新对象,随后再挂起执行。生成器在这个状态暂停并等待继续执行。
    • 完成 — 在生成器执行期间,如果代码执行到return语句或者全部代码执行完毕,生成器就进入该状态。

promise

    • promise实例化对象传入的是两个函数参数,第一个为resolve函数表成功,reject表失败;
    • 当承诺成功兑现(在promise上调用了resolve),前一个回调就会被调用,而当出现错误就会调用后一个回调函数(可以是发生了一个未处理的异常,也可以是在promise上调用了reject)
    • promise 对象是对我们现在尚未得到但将来会得到值的占位符;
    • 它是对我们最终能够得知异步计算结果的一种保证。如果我们兑现了我们的承诺,那结果会得到一个值。如果发生了问题,结果则是一个错误,一个为什么不能交付的借口。
  • 回调函数缺陷:

    • 错误难以处理:不是很理解这段话,学完再看;
    • 执行连续步骤麻烦:
      • 一个长期任务结束后我们可能会用得到的数据开启另一项任务,就需要不停的缩进+嵌套;
    • 并列步骤需要书写多段类似代码:
  • promise执行逻辑:

      • promise对象从pending开始,标记为未完成;
      • 若promise对象resolve方法被调用,获取值,进入完成状态;
      • 若reject方法被调用,则获取出错原因,进入完成状态;
      • 需要注意的是第二个Inmmediatepromise,为什么会在”at code end”后执行?
  • 拒绝promise:
    • then中调用第二个回调函数
    • then中只传入第一个回调函数, 错误通过catch获取
      • then可以有很多步,从而完成任务流,而catch函数,只要前面有任何一个promise出错,就会将其捕捉;
      • 不是主动调用,而是函数内部出错;
      • Promise.all函数接受的是一个promise对象的数组;
      • 只有全部成功才会被解决, 只要有一个失败就被拒绝;
      • 拒绝与接受取决于第一个成功的promise;

生成器与promise结合

    • 每个promise也即异步任务都被yield返回;
    • 如果生成器的结果是一个被成功兑现的承诺,我们就是用迭代器的 next 方法把承诺的值返回给生成器并恢复执行iteratorValue.then(res => handle(iterator.next(res)));
    • 如果出现错误,承诺被违背,我们就使用迭代器的throw方法抛出一个异常.catch(err => iterator.throw(err))