[关闭]
@Shel 2016-06-28T10:32:05.000000Z 字数 47920 阅读 1023

JavaScript

Study JS

Author: She Liu
Date: 2016-05-11
Location: Suzhou, China



陈芝麻,烂谷子

在上个世纪的1995年,当时的网景公司正凭借其 Navigator 浏览器成为 Web 时代开启时最著名的第一代互联网公司。由于网景公司希望能在静态 HTML页面上添加一些动态效果,于是叫 Brendan Eich 这哥们在两周之内设计出了 JavaScript 语言。你没看错,这哥们只用了10天时间。为什么起名叫 JavaScript ?原因是当时 Java 语言非常红火,所以网景公司希望借 Java 的名气来推广,但事实上 JavaScript 除了语法上有点像 Java ,其他部分基本上没啥关系。

一年后微软又模仿 JavaScript 开发了 JScript ,为了让 JavaScript 成为全球标准,几个公司联合 ECMA(European Computer Manufacturers Association)组织定制了 JavaScript 语言的标准,被称为 ECMAScript 标准。

所以简单说来就是,ECMAScript 是一种 语言标准 ,而 JavaScript 是网景公司对 ECMAScript 标准的一种 实现

浏览器大战

微软通过IE击败了Netscape后一统桌面,结果几年时间,浏览器毫无进步。(2001年推出的古老的IE 6到今天仍然有人在使用!)没有竞争就没有发展。微软认为IE6浏览器已经非常完善,几乎没有可改进之处,然后解散了IE6开发团队!而Google却认为支持现代Web应用的新一代浏览器才刚刚起步,尤其是浏览器负责运行JavaScript的引擎性能还可提升10倍。

先是Mozilla借助已壮烈牺牲的Netscape遗产在2002年推出了Firefox浏览器,紧接着Apple于2003年在开源的KHTML浏览器的基础上推出了WebKit内核的Safari浏览器,不过仅限于Mac平台。随后,Google也开始创建自家的浏览器。他们也看中了WebKit内核,于是基于WebKit内核推出了Chrome浏览器。

随后,Google也开始创建自家的浏览器。他们也看中了WebKit内核,于是基于WebKit内核推出了Chrome浏览器。Chrome浏览器是跨Windows和Mac平台的,并且,Google认为要运行现代Web应用,浏览器必须有一个性能非常强劲的JavaScript引擎,于是Google自己开发了一个高性能JavaScript引擎,名字叫V8,以BSD许可证开源。

现代浏览器大战让微软的IE浏览器远远地落后了,因为他们解散了最有经验、战斗力最强的浏览器团队!回过头再追赶却发现,支持HTML5的WebKit已经成为手机端的标准了,IE浏览器从此与主流移动端设备绝缘。

它是世界之最

JavaScript 是世界上最流行的脚本语言,因为你在电脑、手机、平板上浏览的所有的网页,以及无数基于 HTML5 的手机App,交互逻辑都是由 JavaScript 驱动的。

简单地说,JavaScript 是一种运行在浏览器中的 解释型 的编程语言。

那么问题来了,为什么我们要学 JavaScript?尤其是当你已经掌握了某些其他编程语言如 Java、C++ 的情况下。简单粗暴的回答就是:因为你没有选择。在 Web 世界里,只有 JavaScript 能跨平台、跨浏览器驱动网页,与用户交互。

JavaScript 一度被认为是一种玩具编程语言,它有很多缺陷,所以不被大多数后端开发人员所重视。很多人认为,写 JavaScript 代码很简单,并且 JavaScript 只是为了在网页上添加一点交互和动画效果。但这是完全错误的理解。JavaScript 确实很容易上手,但其精髓却不为大多数开发人员所熟知。编写高质量的 JavaScript 代码更是难上加难。

不仅仅是前端

话说有个外国人叫Ryan Dahl,他的工作是用C/C++写高性能Web服务。对于高性能,异步IO、事件驱动是基本原则,但是用C/C++写就太痛苦了。于是这位仁兄开始设想用高级语言开发Web服务。他评估了很多种高级语言,发现很多语言虽然同时提供了同步IO和异步IO,但是开发人员一旦用了同步IO,他们就再也懒得写异步IO了,所以,最终,Ryan瞄向了JavaScript。

选定了开发语言,还要有运行时引擎。这位仁兄曾考虑过自己写一个,不过明智地放弃了,因为V8就是开源的JavaScript引擎。让Google投资去优化V8,咱只负责改造一下拿来用,还不用付钱,这个买卖很划算。

于是在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。虽然名字很土,但是,Node第一次把JavaScript带入到后端服务器开发,加上世界上已经有无数的JavaScript开发人员,所以Node一下子就火了起来。


JS 入门篇

1. 语法

1.1. 条件判断

if () { ... } else { ... }

在多个 if...else... 语句中,如果某个条件成立,则后续就不再继续判断了。这句话看似简单,但活用它的人却不多。

有时,条件判断语句的结果不是布尔值,这在实际情况中很常见,不用担心,JS 是一门十分友好的语言,它把null、undefined、0、NaN和空字符串''视为false,其他视为 true。

1.2. 循环

for循环、for ... in、for ... of、while、do ... while

for 循环,通过初始条件、结束条件和递增条件来循环执行语句块。for循环最常用的地方是利用索引来 遍历数组

for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来。由于Array也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in循环可以直接循环出Array的索引.

  1. var o = {
  2. name: 'Jack',
  3. age: 20,
  4. city: 'Beijing'
  5. };
  6. for (var key in o) {
  7. if (o.hasOwnProperty(key)) {
  8. alert(key); // 'name', 'age', 'city'
  9. }
  10. }
  11. var a = ['A', 'B', 'C'];
  12. for (var i in a) {
  13. alert(i); // '0', '1', '2'
  14. alert(a[i]); // 'A', 'B', 'C'
  15. }

for ... of 循环是 ES6 引入的新的语法,它只循环集合本身的元素。

  1. var a = ['A', 'B', 'C'];
  2. a.name = 'Hello';
  3. for (var x in a) {
  4. alert(x); // '0', '1', '2', 'name'
  5. }
  6. for (var x of a) {
  7. alert(x); 'A', 'B', 'C'
  8. }

2. 数据类型

2.1. Number

  • 123; // 整数123
  • 0.456; // 浮点数0.456
  • 1.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5
  • -99; // 负数
  • NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示
  • Infinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就表示为Infinity

2.2. 字符串

字符串是以单引号'或双引号"括起来的任意文本。

在ES6之前多行字符串用\n来表示,这很麻烦不是吗?ES6对此进行了增强,就是用反引号 `!

对于字符串,我们一般会使用到的操作有:获取某个指定位置的字符、以及一些变换

  1. var s = 'Hello, world!';
  2. s[0]; // 'H'
  3. s[6]; // ' '
  4. s[13]; // undefined 超出范围的索引不会报错,但一律返回undefined字符串是不可变的,如果对字符串的某个索引赋值,不会有任何错误,但是,也没有任何效果
  5. var s = 'Test';
  6. s[0] = 'X';
  7. alert(s); // s仍然为'Test'

JavaScript为字符串提供了一些常用方法,注意,调用这些方法本身 不会改变原有字符串的内容 ,而是返回一个 新字符串

  • toUpperCase()把一个字符串全部变为大写:
  • toLowerCase()把一个字符串全部变为小写:
  • indexOf()会搜索指定字符串出现的位置:
  • substring()返回指定索引区间的子串:

2.3. 布尔值:true、false 两种

千万别小看了这俩家伙,我们经常会用到 == 或是 === 两种比较运算符,前者会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;后者不会自动转换数据类型,如果数据类型不一致,返回 false,如果一致,再比较。以防万一,我们尽量使用后者吧。

另一个例外是NaN 这个特殊的Number与所有其他值都不相等,包括它自己,唯一能判断的方法是通过函数 isNaN() ,最后要注意浮点数的相等比较:浮点数在运算过程中会产生误差,因为计算机无法精确表示无限循环小数。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值:

  1. NaN === NaN; // false
  2. isNaN(NaN); // true
  3. 1 / 3 === (1 - 2 / 3); // false
  4. Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true

2.4. null 和 undefined

  • null:表示一个“空”的值
  • ‘’:表示长度为0的字符串
  • 0:是一个数值
  • undefined:表示“未定义”

2.5. 数组

一组按顺序排列的集合,集合的每个值称为元素。

JavaScript 的数组可以包括任意数据类型,并通过索引来访问每个元素。要取得 Array 的长度,直接访问 length 属性,直接给 Array 的 length 赋一个新的值会导致 Array 大小的变化,这很有趣~具体看下面的例子。

  1. var arr = [1, 2, 3];
  2. arr.length; // 3
  3. arr.length = 6;
  4. arr; // arr变为[1, 2, 3, undefined, undefined, undefined]
  5. arr.length = 2;
  6. arr; // arr变为[1, 2]
  7. arr[5] = 'x';
  8. arr; // arr变为[1, 2, 3, undefined, undefined, 'x']

和字符串不一样的是,通过索引是可以改变元素的内容的,也就是说整个array是可以修改的。

  • indexOf():与 String 类似,Array 也可以通过 indexOf() 来搜索一个指定的元素的位置
  • slice():就是对应 String 的 substring() 版本,它截取 Array 的部分元素,然后返回一个新的 Array。如果不给 slice() 传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个 Array。
  • push():向 Array 的末尾添加若干元素。
  • pop():把 Array 的最后一个元素删除掉。
  • unshift():头部添加若干元素。
  • shift():第一个元素删掉。
  • reverse():把整个 Array 的元素给掉个个,也就是反转。
  • sort():可以对当前 Array 进行排序,它会直接修改当前 Array 的元素位置,直接调用时,按照默认顺序排序,当然也可以按照我们指定的方式排序。
  • splice():“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素。
  • concat():把当前的 Array 和另一个 Array 连接起来,并返回一个新的 Array,它并没有修改当前的数组,而是生成了一个新的数组,所以最好是用一个新的变量来承接它。另外,这个方法可以接受任意个元素和 Array,它会把他们拆开,然后全部添加到新的数组里。
  • join():这是一个非常实用的方法,它把当前 Array 的每个元素都用指定的字符串连接起来,然后返回连接后的字符串。
  1. var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
  2. arr.splice(2, 3, 'Google', 'Facebook');
  3. /* 从索引2开始删除3个元素,然后再添加两个元素,返回删除的元素 ['Yahoo', 'AOL', 'Excite'],而此时 arr 的值是['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']*/
  4. var arr = ['A', 'B', 'C'];
  5. arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]
  6. var arr = ['A', 'B', 'C', 1, 2, 3];
  7. arr.join('-'); // 'A-B-C-1-2-3'

2.6. 对象

一组由键-值组成的无序集合.

JavaScript 对象的键都是字符串类型,值可以是任意数据类型。

其中每个键又称为对象的属性,要获取一个对象的属性,我们用 “对象变量.属性名” 的方式,但这要求属性名必须是一个有效的变量名。如果属性名包含特殊字符,就必须用单引号或者双引号括起来,至于为什么要用引号,请看上一条吧!
实际上我们不建议使用有特殊字符的 键 ,因为访问的时候也要用 “object[prop]” 的形式,而不能使用之前说的那种简洁方式.

如果访问一个不存在的属性会返回什么呢?JavaScript规定,访问不存在的属性不报错,而是返回undefined,也就是“未定义”。这听起来很合乎常理。

我们可以很自由的给一个对象添加或删除属性,但往往还会检测是否拥有某一种属性,这里就使用到一个特殊的操作符 in ,但这个操作符是无法区分属性来自原型链,还是它自己的。也就是无法区分属性是否是继承得到的。要判断这一点还有一个可行的办法是使用 hasOwnProperty() 方法。

  1. var xiaoming = {
  2. name: '小明',
  3. birth: 1990,
  4. school: 'No.1 Middle School',
  5. height: 1.70,
  6. weight: 65,
  7. score: null
  8. };
  9. 'name' in xiaoming; // true
  10. 'grade' in xiaoming; // false
  11. 'grade' in xiaoming; // false
  12. xiaoming.hasOwnProperty('name'); // true
  13. xiaoming.hasOwnProperty('toString'); // false

一个 Array 数组实际上也是一个对象,它的每个元素的索引被视为一个属性。前面有提到可以使用for ... in循环遍历对象属性。由于历史遗留问题,它遍历的实际上是对象的属性名称。当我们手动给 Array对象 添加了额外的属性后,for ... in 循环将带来意想不到的意外效果。

  1. var a = ['A', 'B', 'C'];
  2. a.name = 'Hello';
  3. for (var x in a) {
  4. alert(x); // '0', '1', '2', 'name',循环将把 name 包括在内,但 Array 的 length 属性却不包括在内。
  5. }

2.7. Map

ES6标准新增的数据类型

javascript对象的键必须是字符串,实际上使用其他数据类型也是合理的,为了弥补这个缺陷,最新的ES6规范引入了新的数据类型 Map。
Map是一组键值对的结构,具有极快的查找速度。
经常会用到的操作方法是 set、get、has、delete

  1. var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
  2. m.get('Michael'); // 95
  3. var m = new Map(); // 空Map
  4. m.set('Adam', 67); // 添加新的key-value
  5. m.set('Bob', 59);
  6. m.has('Adam'); // 是否存在key 'Adam': true
  7. m.get('Adam'); // 67
  8. m.delete('Adam'); // 删除key 'Adam'
  9. m.get('Adam'); // undefined

2.8. Set

ES6标准新增的数据类型

Set 和 Map 类似,也是一组 key 的集合,但不存储 value。由于 key 不能重复,所以,在 Set 中,没有重复的 key。要创建一个 Set,需要提供一个 Array 作为输入,或者直接创建一个空 Set,重复元素在 Set 中自动被过滤。
经常会用到的操作方法是 add(key)、delete(key)

2.9. iterable

ES6标准新增的数据类型

遍历 Array 可以采用下标循环,遍历 Map 和 Set 就无法使用下标。为了统一集合类型,ES6标准引入了新的 iterable 类型,Array、Map 和 Set 都属于 iterable 类型。

具有 iterable 类型的集合可以通过新的 for ... of 循环来遍历。
然而,更好的方式是直接使用 iterable 内置的 forEach 方法,它接收一个函数,每次迭代就自动回调该函数。

  1. var a = ['A', 'B', 'C'];
  2. a.forEach(function (element, index, array) {
  3. // element: 指向当前元素的值
  4. // index: 指向当前索引
  5. // array: 指向Array对象本身
  6. alert(element);
  7. });
  8. //Set 没有索引,因此回调函数的前两个参数都是元素本身
  9. var s = new Set(['A', 'B', 'C']);
  10. s.forEach(function (element, sameElement, set) {
  11. alert(element);
  12. });
  13. //Map的回调函数参数依次为value、key和map本身
  14. var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
  15. m.forEach(function (value, key, map) {
  16. alert(value);
  17. });

ps:JavaScript 的函数调用不要求参数必须一致,我们可以只列出我们需要的参数,比如上述第一个函数function (element, index, array)是可以写成function (element)这个样子的,至于为什么可以这样做,这个话题就比较深了,不在这儿展开。

3. 变量

变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。

如果一个变量没有通过var 申明就被使用,那么该变量就自动被申明为全局变量,初学者常常会因这样的失误而导致一些不理解的错误,为了便于开发的进行——启用strict模式。

'use strict';

这是一个字符串,不支持 strict 模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启 strict 模式运行 JavaScript 。


JS 进阶篇

1. 函数

基本上所有的高级语言都支持函数,JavaScript也不例外。JavaScript的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力。

抽象是数学中非常常见的概念。

借助抽象,我们才能不关心底层的具体计算过程,而直接在更高的层次上思考问题。

写计算机程序也是一样,函数就是最基本的一种代码抽象的方式。

1.1. 函数定义和调用

关键字:function

一个函数也是一个对象,函数名可以视为指向该函数的变量名。

调用函数的时候可以传入任意数量的参数,传入的参数比实际使用到的多或是少都没有问题。要避免接收到未定义的参数,可以对参数进行检查:

  1. function abs(x) {
  2. if (typeof x !== 'number') {
  3. throw 'Not a number';
  4. }
  5. if (x >= 0) {
  6. return x;
  7. } else {
  8. return -x;
  9. }
  10. }

关键字:arguments

它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。它是一个类似数组但不是数组的特殊对象。利用它,计时函数未定义任何参数,我们还是能够拿到参数的值。实际上我们最常用到的是判断传入参数的个数。

  1. // foo(a[, b], c)
  2. // 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
  3. function foo(a, b, c) {
  4. if (arguments.length === 2) {
  5. // 实际拿到的参数是a和b,c为undefined
  6. c = b; // 把b赋给c
  7. b = null; // b变为默认值
  8. }
  9. // ...
  10. }

关键字:rest(ES6新引入)

arguments 可以获取到所有的参数,而rest 可以获取除开定义以外的参数。

如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。

写法有些特殊,只能用在最后,前面要用...标志

关键字: return

函数体内部的语句在执行时,一旦执行到 return时,函数就执行完毕,并将结果返回。如果没有 return语句,函数执行完毕后也会返回结果,只是结果为 undefined

必须小心return 语句。我们知道 JavaScript 引擎有一个在行末自动添加分号的机制。如果把return语句拆成两行,呵呵……

  1. function foo() {
  2. return
  3. { name: 'foo' };
  4. }
  5. function foo() {
  6. return; // 自动添加了分号,相当于return undefined;
  7. { name: 'foo' }; //这行语句已经没法执行到了
  8. }

最好的做法是永远在 return 后面跟上大括号。

1.2. 变量作用域

前面提到过变量是用 var 声明的。
如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量,此时我们称该变量为局部变量。

JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

变量提升

是JS函数定义的特点,JavaScript引擎会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部,但提升的仅仅是声明,变量的赋值并不会提升,这个怪异特性无疑会给我们编程造成一定的影响,因此,我们建议最好的做法是“先声明,后使用”

  1. function foo() {
  2. var x = 'Hello, ' + y;
  3. alert(x);//alert显示Hello, undefined
  4. var y = 'Bob';
  5. }

如果不用关键字声明,则变量默认为全局变量。实际上,JavaScript 默认有一个全局对象 window,全局作用域的变量实际上被绑定到 window 的一个属性。

命名冲突

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore

  1. // 唯一的全局变量MYAPP:
  2. var MYAPP = {};
  3. // 其他变量:
  4. MYAPP.name = 'myapp';
  5. MYAPP.version = 1.0;
  6. // 其他函数:
  7. MYAPP.foo = function () {
  8. return 'foo';
  9. };

关键字:let(ES6新增)

以前,函数内部的语句块中是无法定义具有局部作用域的变量的,JS中没有 块级作用域 的概念。为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量。

  1. function foo() {
  2. for (var i=0; i<100; i++) {
  3. //
  4. }
  5. i += 100; // 仍然可以引用变量i
  6. }
  7. function foo() {
  8. var sum = 0;
  9. for (let i=0; i<100; i++) {
  10. sum += i;
  11. }
  12. i += 1; // SyntaxError
  13. }

关键字:const(ES6新增)

ES6之前,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值(实际上我们还是可以更改它的)”。
ES6标准引入了新的关键字const来定义常量。并且,它也具有 块级作用域

1.3. 方法

对象是“键-值”的集合,“值”可以是任意数据类型,包括函数这个特殊的对象。绑定到对象上的函数称为方法。

特殊变量:this

让我们梳理一下,对象里有属性-属性值,属性值可以是函数(我们将这个函数叫做对象的方法)。因此,方法是对象的,对象内有方法。在一个方法的内部,this是一个特殊的变量,前面提到过函数内部定义的变量有局部作用域,但this 这个特殊的变量是始终指向当前对象的。我想看到这儿读者多半开始蒙了,什么叫当前对象?如果我单独声明了一个函数,而在函数内部又调用了this,那么它应该指向谁?如果我又将这个函数赋给一个对象,作为对象的方法来调用,那么这个this 有指向谁?晕没晕?!呵呵~淡定~

this 的指向视情况而定,但我们只需要记住这句话就不用担心搞混了—— 始终指向当前对象

  1. function getAge() {
  2. var y = new Date().getFullYear();
  3. return y - this.birth;
  4. }
  5. getAge();
  6. // NaN;这里声明了一个函数,如果直接调用,此时的this指向的当前对象是全局对象window~所以它返回的结果是NaN。
  7. var xiaoming = {
  8. name: '小明',
  9. birth: 1990,
  10. age: getAge
  11. };
  12. xiaoming.age();
  13. // 25, 正常结果,当做方法调用,this指向的当前对象是xiaoming!!!
  14. var fn = xiaoming.age;
  15. // 先拿到xiaoming的age函数
  16. fn();
  17. // NaN,这种方法也是不行的,this指向window,要保证this的指向,千万记得用object.xxx()的形式来调用。

ECMA决定,在strict模式下让函数的this指向undefined。这个决定只是让错误及时暴露出来,并没有解决this应该指向的正确位置。

  1. 'use strict';
  2. var xiaoming = {
  3. name: '小明',
  4. birth: 1990,
  5. age: function () {
  6. function getAgeFromBirth() {
  7. var y = new Date().getFullYear();
  8. return y - this.birth;
  9. }
  10. return getAgeFromBirth();
  11. }
  12. };
  13. xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined

又报错了。thisage方法内指向对象xiaoming,但在方法内部又定义函数,this又指向了window,严格模式下就是undefined。解决方法是什么呢?用一个变量先捕获this

  1. 'use strict';
  2. var xiaoming = {
  3. name: '小明',
  4. birth: 1990,
  5. age: function () {
  6. var that = this; // 在方法内部一开始就捕获this
  7. function getAgeFromBirth() {
  8. var y = new Date().getFullYear();
  9. return y - that.birth;
  10. // 用that而不是this
  11. }
  12. return getAgeFromBirth();
  13. }
  14. };
  15. xiaoming.age(); // 25

特殊方法:apply、 call

函数是一个特殊的对象,所以函数也可以具备方法,函数本身的apply方法就是专门用来整治this的。

它接收两个参数,第一个参数就是需要绑定的this 变量,第二个参数是 Array,表示函数本身的参数。

另一个与apply()类似的方法是call(),唯一区别是:apply()把参数打包成Array再传入;call()把参数按顺序传入。

  1. function getAge() {
  2. var y = new Date().getFullYear();
  3. return y - this.birth;
  4. }
  5. var xiaoming = {
  6. name: '小明',
  7. birth: 1990,
  8. age: getAge
  9. };
  10. xiaoming.age(); // 25
  11. getAge.apply(xiaoming, []);
  12. // 25, this指向xiaoming, 参数为空
  13. /*这是一个很有用的例子*/
  14. var count = 0;
  15. var oldParseInt = parseInt; // 保存原函数
  16. window.parseInt = function () {
  17. count += 1;
  18. return oldParseInt.apply(null, arguments); // 调用原函数
  19. };
  20. // 测试:
  21. parseInt('10');
  22. parseInt('20');
  23. parseInt('30');
  24. count; // 3

1.4. 高阶函数

高阶函数英文叫Higher-order function。

JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为 高阶函数

有四个必须掌握的函数 map遍历reduce迭代filter筛选sort排序

举例说明,比如我们有一个函数 f(x) ,要把这个函数作用在一个数组 [1, 2, 3, 4, 5, 6, 7, 8, 9] 上,就可以用map实现,由于map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一个新的Array作为结果。你可能会想,不需要map(),写一个循环,也可以计算出结果,的确可以。map() 作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x),还可以计算任意复杂的函数,比如,把Array的所有数字转为字符串.
但要注意一点,使用map改变Array的每项元素,会使得数组对象整体发生变化。

  1. function pow(x) {
  2. return x * x;
  3. }
  4. var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  5. arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
  6. //手动循环
  7. var result = [];
  8. for (var i=0; i<arr.length; i++) {
  9. result.push(f(arr[i]));
  10. }

这也是一个Array的方法,它接收一个函数,并把这个函数作用在Array的每一个元素上,这个函数必须接收两个参数,与map() 不同的是reduce() 把结果继续和序列的下一个元素做累积计算,因而它的执行过程更类似于串行执行,map() 更类似于并行执行,reduce() 每个元素都依赖于上一个元素[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4).但使用reduce处理的数组自身并没有发生改变。改变Array的每项元素,会使得数组对象整体发生变化。

它用于把Array的某些元素过滤掉,然后返回剩下的元素。filter()也接收一个函数,传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。它会返回一个新的数组对象,而原数组对象是没有发生改变的。

  1. var arr = [1, 2, 4, 5, 6, 9, 10, 15];
  2. var r = arr.filter(function (x) {
  3. return x % 2 !== 0;
  4. });
  5. r; // [1, 5, 9, 15]

排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。

如果是数字,我们可以直接比较,但如果是字符串或者两个对象呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

通常规定,对于两个元素x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。

需要注意,Arraysort()方法默认把所有元素先转换为String再排序,默认情况下,对字符串排序,是按照ASCII的大小比较的。如果不知道sort()方法的默认排序规则,直接对数字排序,绝对栽进坑里!幸运的是,sort() 方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。

最后友情提示,sort()方法会直接对Array进行修改,它返回的结果仍是当前Array。

  1. [10, 20, 1, 2].sort(); // [1, 10, 2, 20]
  2. var arr = [10, 20, 1, 2];
  3. arr.sort(function (x, y) {
  4. if (x < y) {
  5. return -1;
  6. }
  7. if (x > y) {
  8. return 1;
  9. }
  10. return 0;
  11. }); // [1, 2, 10, 20]

1.5. 闭包

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回

  1. function lazy_sum(arr) {
  2. var sum = function () {
  3. return arr.reduce(function (x, y) {
  4. return x + y;
  5. });
  6. }
  7. return sum;
  8. }
  9. var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
  10. f(); // 15
  11. /*当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数。调用函数f时,才真正计算求和的结果*/

注意到返回的函数在其定义内部引用了局部变量arr,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。

  1. function count() {
  2. var arr = [];
  3. for (var i=1; i<=3; i++) {
  4. arr.push(function () {
  5. return i * i;
  6. });
  7. }
  8. return arr;
  9. }
  10. var results = count();
  11. var f1 = results[0];
  12. var f2 = results[1];
  13. var f3 = results[2];
  14. f1(); // 16
  15. f2(); // 16
  16. f3(); // 16

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变,因为函数参数相当于函数内的局部变量。

  1. function count() {
  2. var arr = [];
  3. for (var i=1; i<=3; i++) {
  4. arr.push((function (n) {
  5. return function () {
  6. return n * n;
  7. }
  8. })(i));
  9. }
  10. return arr;
  11. }

说了这么多,难道闭包就是为了返回一个函数然后延迟执行吗?在没有 class 机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。来看一个十分经典的例子:计数器。

  1. function create_counter(initial) {
  2. var x = initial || 0;
  3. return {
  4. inc: function () {
  5. x += 1;
  6. return x;
  7. }
  8. }
  9. }
  10. var c1 = create_counter();
  11. c1.inc(); // 1
  12. c1.inc(); // 2
  13. c1.inc(); // 3
  14. var c2 = create_counter(10);
  15. c2.inc(); // 11
  16. c2.inc(); // 12
  17. c2.inc(); // 13

1.6. 箭头函数

ES6标准新增了一种新的函数:Arrow Function(箭头函数)。它的定义用的就是一个箭头,很直白吧~

  1. x => x * x
  2. //相当于
  3. function (x) {
  4. return x * x;
  5. }

箭头函数看上去像是简化版的匿名函数。格式如下:

  1. x => {
  2. if (x > 0) {
  3. return x;
  4. }
  5. else {
  6. return 0;
  7. }
  8. }
  9. // 两个参数:
  10. (x, y) => x * x + y * y
  11. // 无参数:
  12. () => 3.14
  13. // 可变参数:
  14. (x, y, ...rest) => {
  15. var i, sum = x + y;
  16. for (i=0; i<rest.length; i++) {
  17. sum += rest[i];
  18. }
  19. return sum;
  20. }
  21. // 如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错
  22. x => { foo: x };
  23. // 因为和函数体的{ ... }有语法冲突,所以要改为
  24. x => ({ foo: x })

箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:就是那个该死的特殊变量this

箭头函数内部的this是词法作用域,由上下文确定。这修复了之前的问题,使得this永远指向外层调用者。

由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略.

  1. var obj = {
  2. birth: 1990,
  3. getAge: function () {
  4. var b = this.birth; // 1990
  5. var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象,而不是getAge 函数对象。
  6. return fn();
  7. }
  8. };
  9. obj.getAge(); // 25

1.7. generator

回忆一下,函数在执行过程中,如果没有遇到return语句(函数末尾如果没有return,就是隐含的return undefined;),控制权无法交回被调用的代码。

generator(生成器)是ES6标准引入的新的数据类型。

一个generator看上去像一个函数,但可以返回多次。

  1. function* foo(x) { // 注意多出的*号
  2. yield x + 1; // 关键字yield,用于多次返回。
  3. yield x + 2;
  4. return x + 3; // 常规返回
  5. }

generator的调用方式有些特殊,有两种,依旧是上面的例子:

  1. ...
  2. foo(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window} /*由此可见, 并没有执行函数,仅仅是创建了一个generator对象.*/
  3. var f = f00(5);
  4. f.next(); // {value: 6, done: false} 它返回了一个对象
  5. f.next(); // {value: 7, done: false}
  6. f.next(); // {value: 8, done: true}

next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。

第二个方法是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done。

  1. for (var x of fib(5)) {
  2. console.log(x); // 依次输出0, 1, 1, 2, 3
  3. }

由此可以看出,与普通的函数对象相比,generator像是被切成块的函数,一块一块执行。每执行一块就会暂停,并交回代码的控制权。仿佛为函数标记了执行的状态。想想我们以前是怎么处理的,闭包!有了它我们就可以不用再使用那个痛苦的玩意了!


2. 对象(标准包)

至此我们脑中因该有一个概念,一提到对象,请立马想到用大括号包裹起来的一堆键值对{...}。这是我们通常意义上的对象,但不得不承认的是,“普遍中总有特例”,而特例往往会更引起人们的重视。

在前面基础篇的时候,我们提到了数据的类型,还记得吗,结合上面我们所学的,一起来回忆一下:

number、string、boolean、Array、Map、Set、iterable、null、undefined、object、function、generator

在JavaScript的世界里,一切都是对象。number、string、boolean有与之对应的包装对象。包装对象用new创建。虽然包装对象看上去和原来的值一模一样,显示出来也是一模一样,但他们的类型已经变为object了!所以,包装对象和原始值用===比较会返回false。所以尽量不要使用包装对象。

总结一下,有这么几条规则需要遵守:

2.1. Date

在JavaScript中,Date对象用来表示日期和时间。

2.2. RegExp

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

  • 在正则表达式中,如果直接给出字符,就是精确匹配。
    • \d可以匹配一个数字;
    • \w可以匹配一个字母或数字;
    • .可以匹配任意字符;
    • \s可以匹配一个空格(也包括Tab等空白符)
  • 要匹配变长的字符,在正则表达式中
    • *表示任意个字符(包括0个),
    • +表示至少一个字符,
    • ?表示0个或1个字符,
    • {n}表示n个字符,
    • {n,m}表示n-m个字符
  • 要做更精确地匹配,可以用[]表示范围
    • [0-9a-zA-Z\_] 可以匹配一个数字、字母或者下划线;
    • [0-9a-zA-Z\_]+ 可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100','0_Z','js2015'等等
    • [a-zA-Z\_\$][0-9a-zA-Z\_\$]* 可以匹配由字母或下划线、线组成的字符串,也就是JavaScript允许的变量名;
    • [a-zA-Z\_\$][0-9a-zA-Z\_\$]{0, 19} 更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
  • 对于特殊字符,在正则表达式中,要用'\'转义
  • A|B表示可以匹配A或B;
  • ^表示行的开头,^\d表示必须以数字开头。
  • $表示行的结束,\d$表示必须以数字结束。

JavaScript有两种方式创建一个正则表达式:第一种方式是直接通过/正则表达式/写出来,第二种方式是通过new RegExp('正则表达式')创建一个RegExp对象。

  1. var re1 = /ABC\-001/;
  2. var re2 = new RegExp('ABC\\-001');
  3. //RegExp对象的test()方法用于测试给定的字符串是否符合条件。
  4. var re = /^\d{3}\-\d{3,8}$/;
  5. re.test('010-12345'); // true

2.3. JSON

JSON是JavaScript Object Notation的缩写,它是一种数据交换格式。

在JSON出现之前,大家一直用XML来传递数据。XML本身不算复杂,但是,加上DTD、XSD、XPath、XSLT等一大堆复杂的规范以后,任何正常的软件开发人员碰到XML都会感觉头大了,最后大家发现,即使你努力钻研几个月,也未必搞得清楚XML的规范。终于,在2002年的一天,道格拉斯·克罗克福特(Douglas Crockford)同学为了拯救深陷水深火热同时又被某几个巨型软件企业长期愚弄的软件工程师,发明了JSON这种超轻量级的数据交换格式。

  • JSON定死了字符集必须是UTF-8,这解决了多语言问题;
  • JSON的字符串规定必须用双引号"",Object的键也必须用双引号"",这使得统一解析成了可能;
  • 定死数据格式number、boolean、string、null、array(就是JavaScript的Array表示方式——[])、object(就是JavaScript的{ ... }表示方式)

JavaScript内置了JSON的解析,把任何JavaScript对象变成JSON,就是把这个对象序列化成一个JSON格式的字符串,这样才能够通过网络传递给其他计算机。如果我们收到一个JSON格式的字符串,只需要把它反序列化成一个JavaScript对象,就可以在JavaScript中直接使用这个对象了。

  1. var xiaoming = {
  2. name: '小明',
  3. age: 14,
  4. gender: true,
  5. height: 1.65,
  6. grade: null,
  7. 'middle-school': '\"W3C\" Middle School',
  8. skills: ['JavaScript', 'Java', 'Python', 'Lisp']
  9. };
  10. JSON.stringify(xiaoming, ['name', 'skills'], ' '); //输出指定的属性,并按缩进输出
  11. JSON.stringify(xiaoming, convert, ' ');//传入一个函数,这样对象的每个键值对都会被函数先处理
  12. function convert(key, value) {
  13. if (typeof value === 'string') {
  14. return value.toUpperCase();
  15. }
  16. return value;
  17. }

3. 面向对象编程(拓展包)

JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?NO!对于大多数的面向对象编程语言来说一定会有两个概念:类 和 实例。很显然的,在前面那么久的讲述中,我们都未曾涉及到这两个概念。

JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。

我们来讲一个例子:假设我们想创建一个“小明”这个具体的“学生”,可我们有没有学生这个类型,有的只是object,按照以往,我们硬生生的创建了“小明”,他有身高、体重、会跑……接着我们又要创建一个叫做“罗伯特”的学生,他有身高、体重、会跑……我们是不是又要苦逼的创建一个新的对象?哦不这太坑爹了,所有能用智力解决的问题请不要使用体力!

我们把“小明”这个对象改名为“学生”,把它作为原型,然后用它来创建出“罗伯特”。

  1. var Student = {
  2. name: 'xiaoming',
  3. height: 1.2,
  4. sex: "boy",
  5. run: function () {
  6. console.log(this.name + ' is running...');
  7. }
  8. };
  9. var robot = {
  10. name: 'robot',
  11. height: 1.5
  12. };
  13. robot.__proto__ = Student;
  14. /* 这里实际上有两个对象,一个是Student,一个是robot,在最后一个语句中,robot的原型指向了Student,看上去仿佛是“继承”*/

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。而且这种指向可以更改的,我们随时能让robot的原型指向另一个原型。但请不要直接用obj.__proto__去改变一个对象的原型,这太危险了~

3.1. Object.create()

Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有。

  1. var Student = {
  2. name: 'xiaoming',
  3. height: 1.2,
  4. sex: "boy",
  5. run: function () {
  6. console.log(this.name + ' is running...');
  7. }
  8. };
  9. function createStudent(name) {
  10. // 基于Student原型创建一个新对象:
  11. var s = Object.create(Student);
  12. // 初始化新对象:
  13. s.name = name;
  14. return s;
  15. }
  16. var robot = createStudent('robot');
  17. robot.__proto__ === Student; // true

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯,最后,如果还没有找到,就只能返回undefined。这就产生了一个“原型链”的概念。原型链上有一个对象具备某些方法后,由他展开的对象就可以直接调用这些方法了。很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。

3.2. new + 构造函数

除了直接用{ ... }创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。举个例子:

  1. //定义一个函数。
  2. function Student(name) {
  3. this.name = name;
  4. this.hello = function () {
  5. alert('Hello, ' + this.name + '!');
  6. }
  7. }
  8. var xiaoming = new Student('小明');
  9. // 但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象。
  10. // 注意,如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this;。

函数也是一个对象,上面 函数Student 和新创建的 xiaoming 的原型链是

Student ----> Function.prototype ----> Object.prototype ----> null
xiaoming ----> Student.prototype ----> Object.prototype ----> null

也就是说,xiaoming的原型指向函数Student的原型。而不是xiaoming的原型对象是Student。

换个说法,xiaoming的原型对象是X,Student.prototype指向的对象也是X,这个X有个属性constructor,指向Student函数。

再换种说法,函数Student有个属性叫做prototype,它指向一个对象X;xiaoming没有prototype这个属性却有__proto__这个属性,这个属性指向它的原型,而这个所谓的原型正好就是对象X。直观的感觉是我们并没有显式的创建X,那它在什么时候创建的呢?在第一次使用new Student()的时候创建了两个对象,第一个就是这个原型对象X,第二个是依据X创建的xiaoming。

使用这种方式,无论是原型X,还是创建的对象xiaoming,都会具有一个constructor属性,它指向函数Student

  1. //优化一下。
  2. function Student(name) {
  3. this.name = name;
  4. }
  5. Student.prototype.hello = function () {
  6. alert('Hello, ' + this.name + '!');
  7. };
  8. var xiaoming = new Student('xiaoming');
  9. var xiaoming = new Student('robot');

上面这个例子才是我们通常使用的创建对象的方式。用第一种方式创建对象,每一个新对象都会具有一个hello方法,这很浪费空间资源,事实上我们只要把共享的属性和方法放在原型对象中就可以达到优化的目的,于是便有了第二种方法。

另外,如果一个函数被定义为用于创建对象的构造函数,但是调用时忘记了写new怎么办?在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。

为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如jslint将可以帮你检测到漏写的new。除此之外,一个常用的解决方式是,把所有的new操作封装进一个函数中,这有几个巨大的有点,一是不需要用new来调用,而是参数的传递非常灵活。

  1. function Student(props) {
  2. this.name = props.name || '匿名'; // 默认值为'匿名'
  3. this.grade = props.grade || 1; // 默认值为1
  4. }
  5. Student.prototype.hello = function () {
  6. alert('Hello, ' + this.name + '!');
  7. };
  8. function createStudent(props) {
  9. return new Student(props || {})
  10. }
  11. var xiaoming = createStudent({
  12. name: '小明'
  13. });

如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。由于参数是一个Object,我们无需记忆参数的顺序。如果恰好从JSON拿到了一个对象,直接丢给函数就可以创建出xiaoming。

3.3. 原型继承

在上面一个小节中,我们使用构造函数 Student 来创建对象,现在我们要基于 Student 扩展出 构造函数 PrimaryStudent,那它的定义如下:

  1. function PrimaryStudent(props) {
  2. // 调用Student构造函数,绑定this变量:
  3. Student.call(this, props);
  4. this.grade = props.grade || 1;
  5. }
  6. /*但是,调用了Student构造函数不等于继承了Student,
  7. new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null,
  8. 而我们想要达到的效果是:
  9. new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null*/

我们不可以直接 PrimaryStudent.prototype = Student.prototype; 如果这样的话,PrimaryStudent和Student共享一个原型对象,那还要定义PrimaryStudent干啥?我们必须借助一个中间对象来实现正确的原型链。为了实现这一点,参考道爷(就是发明JSON的那个道格拉斯)的代码,中间对象可以用一个空函数F来实现。

  1. // PrimaryStudent构造函数:
  2. function PrimaryStudent(props) {
  3. Student.call(this, props);
  4. this.grade = props.grade || 1;
  5. }
  6. // 空函数F:
  7. function F() {
  8. }
  9. // 把F的原型指向Student.prototype:
  10. F.prototype = Student.prototype;
  11. // 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
  12. PrimaryStudent.prototype = new F();
  13. // 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
  14. PrimaryStudent.prototype.constructor = PrimaryStudent;
  15. // 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
  16. PrimaryStudent.prototype.getGrade = function () {
  17. return this.grade;
  18. };
  19. /*注意,函数F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链。如果把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码,也就是复用。*/
  20. function inherits(Child, Parent) {
  21. var F = function () {};
  22. F.prototype = Parent.prototype;
  23. Child.prototype = new F();
  24. Child.prototype.constructor = Child;
  25. }
  26. // JavaScript的原型继承实现方式小结如下:
  27. function Student(props) {
  28. this.name = props.name || 'Unnamed';
  29. }
  30. Student.prototype.hello = function () {
  31. alert('Hello, ' + this.name + '!');
  32. }
  33. //1.定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;
  34. function PrimaryStudent(props) {
  35. Student.call(this, props);
  36. this.grade = props.grade || 1;
  37. }
  38. // 2.实现原型继承链:
  39. inherits(PrimaryStudent, Student);
  40. // 3.继续在新的构造函数的原型上定义新方法
  41. PrimaryStudent.prototype.getGrade = function () {
  42. return this.grade;
  43. };

3.4. class继承

在上面两个小节中,基于原型创建对象,基于原型继承,来来去去晕头转向。

经过很长一段智力上的洗礼,回头看看,JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。

这时候,往往新手要比老手(尤其是有传统面向对象编程基础的)理解得快。在很长的一段时间里,这种构建方式一直没有变更,一直被吐槽,从未被超越。

一直到ES6的年代,新的关键字class终于正式被引入JS。目的就是让定义类更简单。

  1. function Student(name) {
  2. this.name = name;
  3. }
  4. Student.prototype.hello = function () {
  5. alert('Hello, ' + this.name + '!');
  6. }
  7. // 如果用新的class关键字来编写Student,可以这样写:
  8. class Student {
  9. constructor(name) {
  10. this.name = name;
  11. }
  12. hello() {
  13. alert('Hello, ' + this.name + '!');
  14. }
  15. }
  16. var xiaoming = new Student('小明');
  17. xiaoming.hello();
  18. // 基于class的继承
  19. class PrimaryStudent extends Student {
  20. constructor(name, grade) {
  21. super(name); // 记得用super调用父类的构造方法!
  22. this.grade = grade;
  23. }
  24. myGrade() {
  25. alert('I am at grade ' + this.grade);
  26. }
  27. }

比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。

注意PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student。子类的构造函数可能会与父类不太相同,例如,PrimaryStudent需要name和grade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。PrimaryStudent已经自动获得了父类Student的hello方法,我们又在子类中定义了新的myGrade方法。

这与原来的原型继承有什么区别?其实没有。它的作用仅在于简化了编程的代码,而实现的原理还是原来的那一套,只不过这些都交给JavaScript引擎去实现了,就笔者撰写这篇文章的时候并不是所有的主流浏览器都支持ES6的class,因此如果一定要现在就用上,就需要一个工具把class代码转换为传统的prototype代码——请使用预编译工具 Babel


浏览器

  1. IE 6~11:国内用得最多的IE浏览器,历来对W3C标准支持差。从IE10开始支持ES6标准;
  2. Chrome:Google出品的基于Webkit内核浏览器,内置了非常强悍的JavaScript引擎——V8。由于Chrome一经安装就时刻保持自升级,所以不用管它的版本,最新版早就支持ES6了;
  3. Sarafi:Apple的Mac系统自带的基于Webkit内核的浏览器,从 OS X 10.7Lion 自带的6.1版本开始支持ES6,目前最新的OS X 10.10 Yosemite 自带的Sarafi版本是8.x,早已支持ES6;
  4. Firefox:Mozilla自己研制的Gecko内核和JavaScript引擎OdinMonkey。早期的Firefox按版本发布,后来终于聪明地学习Chrome的做法进行自升级,时刻保持最新;
  5. 移动设备上目前iOS和Android两大阵营分别主要使用Apple的Safari和Google的Chrome,由于两者都是Webkit核心,结果HTML5首先在手机上全面普及(桌面绝对是Microsoft拖了后腿),对JavaScript的标准支持也很好,最新版本均支持ES6。

1. 浏览器对象

1.1. window

window 对象不但充当全局作用域,而且表示浏览器窗口。对于窗口来说我们最关心的就是他的宽、高。正好 window 对象有两个属性 innerWidthinnerHeight,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。
对应的,还有一个outerWidthouterHeight属性,可以获取浏览器窗口的整个宽高。

1.2. navigator

navigator 对象表示浏览器的信息,最常用的属性包括:

请注意,navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。很多初学者为了针对不同浏览器编写不同的代码,喜欢用if判断浏览器版本。但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回undefined的特性,直接用短路运算符||计算。

1.3. screen

screen 对象表示屏幕的信息,常用的属性有。

1.4. location

location对象表示当前页面的URL信息。

一个完整的URL可以用location.href获取。要加载一个新页面,可以调用location.assign()。如果要重新加载当前页面,调用location.reload()方法非常方便。

1.5. document

document对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,document对象就是整个DOM树的根节点

要查找DOM树的某个节点,需要从document对象开始查找。最常用的查找是根据ID和Tag Name。

document对象还有一个cookie属性,可以获取当前页面的Cookie。

Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如user=ABC123XYZ(加密的字符串)...,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。

Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。

JavaScript可以通过document.cookie读取到当前页面的Cookie.由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的.如果引入的第三方的JavaScript中存在恶意代码,那就……呵呵了。为了解决这个问题,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持httpOnly选项,IE从IE6 SP1开始支持。

1.6. history

history对象保存了浏览器的历史记录。

JavaScript可以调用history对象的back()forward (),相当于用户点击了浏览器的“后退”或“前进”按钮。

这个对象属于历史遗留对象,对于现代Web页面来说,由于大量使用AJAX和页面交互,简单粗暴地调用history.back()可能会让用户感到非常愤怒。

新手开始设计Web页面时喜欢在登录页登录成功时调用history.back(),试图回到登录前的页面。这是一种错误的方法。

!!!任何情况,你都不应该使用history这个对象了。

2. DOM

由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。始终记住DOM是一个树形结构。

操作一个DOM节点实际上就是这么几个操作:

在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。根据ID可以返回一个DOM节点,根据TagName或ClassName总是返回一组DOM节点。另外,在HTML中,可以使用selector来获取DOM。

严格地讲,我们这里的DOM节点是指Element,但是DOM节点实际上是Node,在HTML中,Node包括Element、Comment、CDATA_SECTION等很多种,以及根节点Document类型,但是,绝大多数时候我们只关心Element,也就是实际控制页面结构的Node,其他类型的Node忽略即可。根节点Document已经自动绑定为全局变量document。

3. 表单

表单本身也是DOM树。不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。

通常如果我们获得了一个<input>节点的引用,就可以直接调用value获得对应的用户输入值,这种方式可以应用于text、password、hidden以及select。

但是,对于单选框和复选框,value属性返回的永远是HTML预设的值,而我们需要获得的实际是用户是否“勾上了”选项,所以应该用checked判断。

HTML5 提供了很多新的表单类型,具体可以参考相关文档。

在获得了用户输入的值后通常会将数据提交给后台服务器,这里有两种常用的个方式来提交表单数据。

方式一:

通过<form>元素的submit()方法提交一个表单。这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击<button type="submit">时提交表单,或者用户在最后一个输入框按回车键。
使用这种方式 button 的 type="button" 并伴随一个onclick的方法。

方式二:
响应<form>本身的onsubmit事件,注意要return true来告诉浏览器继续提交,如果return false,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。

在检查和修改<input>时,要充分利用<input type="hidden">来传递数据。这里举个例子:

  1. <!-- HTML -->
  2. <form id="login-form" method="post" onsubmit="return checkForm()">
  3. <input type="text" id="username" name="username">
  4. <input type="password" id="password" name="password">
  5. <button type="submit">Submit</button>
  6. </form>
  7. <script>
  8. function checkForm() {
  9. var pwd = document.getElementById('password');
  10. // 把用户输入的明文变为MD5:
  11. pwd.value = toMD5(pwd.value);
  12. // 继续下一步:
  13. return true;
  14. }
  15. </script>
  16. <!-- 很多登录表单希望用户输入用户名和口令,但是,安全考虑,提交表单时不传输明文口令,而是口令的MD5。普通JavaScript开发人员会直接修改<input> 这个做法看上去没啥问题,但用户输入了口令提交时,口令框的显示会突然从几个*变成32个*(因为MD5有32个字符)。-->
  17. <!-- HTML -->
  18. <form id="login-form" method="post" onsubmit="return checkForm()">
  19. <input type="text" id="username" name="username">
  20. <input type="password" id="input-password">
  21. <input type="hidden" id="md5-password" name="password">
  22. <button type="submit">Submit</button>
  23. </form>
  24. <script>
  25. function checkForm() {
  26. var input_pwd = document.getElementById('input-password');
  27. var md5_pwd = document.getElementById('md5-password');
  28. // 把用户输入的明文变为MD5:
  29. md5_pwd.value = toMD5(input_pwd.value);
  30. // 继续下一步:
  31. return true;
  32. }
  33. </script>
  34. <!-- 注意到id为md5-password的<input>标记了name="password",而用户输入的id为input-password的<input>没有name属性。没有name属性的<input>的数据不会被提交。 -->

4. 文件

在HTML表单中,可以上传文件的唯一控件就是<input type="file">

当一个表单包含<input type="file">时,表单的enctype必须指定为multipart/form-data,method必须指定为post,浏览器才能正确编码并以multipart/form-data格式发送表单的数据。

出于安全考虑,浏览器只允许用户点击<input type="file">来选择本地文件,用JavaScript对<input type="file">的value赋值是没有任何效果的。当用户选择了上传某个文件后,JavaScript也无法获得该文件的真实路径。

通常,上传的文件都由后台服务器处理,JavaScript可以在提交表单时对文件扩展名做检查,以便防止用户上传无效格式的文件:

  1. var f = document.getElementById('test-file-upload');
  2. var filename = f.value; // 'C:\fakepath\test.png'
  3. if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {
  4. alert('Can only upload image file.');
  5. return false;
  6. }

HTML5的File API提供了FileFileReader两个主要对象,可以获得文件信息并读取文件。

5. AJAX

6. Promise

古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。

在JavaScript的世界中,所有代码都是单线程执行的。这使得所有网络操作,浏览器事件都必须是异步执行。可以使用Promise对象执行异步函数,比如:

  1. // 假设test()是一个异步操作函数
  2. var p1 = new Promise(test);
  3. var p2 = p1.then(function (result) {
  4. console.log('成功:' + result);
  5. });
  6. var p3 = p2.catch(function (reason) {
  7. console.log('失败:' + reason);
  8. });
  9. //Promise对象可以串联起来,所以上述代码可以简化为:
  10. new Promise(test).then(function (result) {
  11. console.log('成功:' + result);
  12. }).catch(function (reason) {
  13. console.log('失败:' + reason);
  14. });

Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离。Promise能够串行执行若干异步任务。也可以并行执行异步任务。比如同时发送两个请求。

  1. // 同时执行p1和p2,并在它们都完成后执行then
  2. Promise.all([p1, p2]).then(function (results) {
  3. console.log(results); // 获得一个Array: ['P1', 'P2']
  4. });
  5. //有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。
  6. Promise.race([p1, p2]).then(function (result) {
  7. console.log(result); // 'P1'
  8. });

我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。想想真是美妙极了~

总结一下:

7. Canvas


jQuery

你可能听说过jQuery,它名字起得很土,但却是JavaScript世界中使用最广泛的一个库。江湖传言,全世界大约有80~90%的网站直接或间接地使用了jQuery。jQuery的理念“Write Less, Do More“,让你写更少的代码,完成更多的工作!

jQuery这么流行,肯定是因为它解决了一些很重要的问题。实际上,jQuery能帮我们干这些事情:
- 消除浏览器差异:你不需要自己写冗长的代码来针对不同的浏览器来绑定事件,编写AJAX等代码;
- 简洁的操作DOM的方法:写$('#test')肯定比document.getElementById('test')来得简洁;
- 轻松实现动画、修改CSS等各种操作。

也是一个合法的变量名,它是变量jQuery的别名。除了可以直接调用外,也可以有很多其他属性。但是,如果变量交出来,然后就只能使用jQuery这个变量。这个黑魔法就是——jQuery.noConflict()。好了,执行完这条语句 $ 就可以拿来干其他事了。

1. jQuery对象

什么是jQuery对象?

jQuery对象类似数组,它的每个元素都是一个引用了DOM节点的对象。

如何创建jQuery对象?

两种方式,分别用于两种情况。
倘若DOM中有相应的node对象,那么使用选择器引用该对象;
倘若没有,直接用 $(...) 就可以了,写法上与前一种并没有区别。

另外,jQuery的选择器不会返回undefined或者null。倘若我们需要查找的DOM不存在,它返回的是[]

jQuery对象和DOM对象之间可以互相转化。

比起DOM对象来说,我们跟喜欢jQuery对象,这显然更加方便。
如果拿到了一个DOM对象,那可以简单地调用$(aDomObject)把它变成jQuery对象,这样就可以方便地使用jQuery的API了。
如果拿到一个jQuery对象,使用.get(n)可以获得对应的DOM对象,n从0开始。

2. 选择器

一般情况下,用上面的选择器已经可以获得我们想要的元素了,但是,当我们拿到一个jQuery对象后,还可以以这个对象为基准,进行查找和过滤。最常见的查找是在某个节点的所有子节点中查找,使用find()方法,它本身又接收一个任意的选择器。这是向下。相反的下上查找使用parent(),对于同级的节点,使用前后查找next()prev()

和函数式编程的map、filter类似,jQuery对象也有类似的方法。—— filter() ,这个方法可以过滤掉不符合选择器条件的节点 。这个方法可以传入函数:

  1. var langs = $('ul.lang li');
  2. langs.filter(function () {
  3. return this.innerHTML.indexOf('S') === 0; // 返回S开头的节点
  4. }); // 拿到Swift, Scheme
  5. //要特别注意函数内部的this被绑定为DOM对象,不是jQuery对象

map()方法把一个jQuery对象包含的若干DOM节点转化为其他对象。它的本质也是一种便利操作。

  1. var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
  2. var arr = langs.map(function () {
  3. return this.innerHTML;
  4. }).get(); // 用get()拿到包含string的Array:['JavaScript', 'Python', 'Swift', 'Scheme', 'Haskell']

此外, 一个jQuery对象如果包含了不止一个DOM节点,first()、last()和slice()方法可以返回一个新的jQuery对象,把不需要的DOM节点去掉。

3. 操作DOM

这么费心费力的找到DOM无非就是要对它进行各种操作(炮制……(⊙﹏⊙)b)

jQuery的API设计非常巧妙:简直是巧夺天工~大神之作~

4. 事件

由于不同的浏览器绑定事件的代码都不太一样,所以用jQuery来写代码,就屏蔽了不同浏览器的差异,我们总是编写相同的代码。

on() OR click():

on方法用来绑定一个事件,我们需要传入事件名称对应的处理函数。另一种是直接调用click方法,只需要传入处理方法就可以了,两种方法完全等价。

jQuery能够绑定的事件主要包括:

  • 鼠标事件
    • click: 鼠标单击时触发;
    • dblclick:鼠标双击时触发;
    • mouseenter:鼠标进入时触发;
    • mouseleave:鼠标移出时触发;
    • mousemove:鼠标在DOM内部移动时触发;
    • hover:鼠标进入和退出时触发两个函数,相当于mouseenter加上mouseleave。
  • 键盘事件:键盘事件仅作用在当前焦点的DOM上,通常是和。
    • keydown:键盘按下时触发;
    • keyup:键盘松开时触发;
    • keypress:按一次键后触发。
      • 其他事件:
      • focus:当DOM获得焦点时触发;
      • blur:当DOM失去焦点时触发;
      • change:当、或的内容改变时触发;
      • submit:当提交时触发;
      • ready:当页面被载入并且DOM树完成初始化后触发。仅作用于document对象。由于ready事件使用非常普遍,所以可以这样简化$(function
        () {...})

off()

一个已被绑定的事件可以解除绑定,与on方法相似,我们需要传入事件名称对应的处理函数,无参数调用时,我们一次性移除所有类型的绑定,只传入事件名称则会移除该事件上绑定的所有事件。

利用jQuery可以反复的绑定事件处理函数,它们会依次执行。

jQuery对这些事件处理函数传入Event对象作为参数。我们可以从这个事件对象上获得跟多信息。

4.1. 事件的触发条件

一般来说,事件的触发总是由用户操作引发的,比如我们监控文本框内容的改动,用户输入文本会导致change事件被触发,但内部使用JavaScript改变文本框的值是不会触发这个事件的。

但有时候我们真的十分需要触发某项事件该怎么办呢?
一种方法是直接调用这个事件,比如,我们希望用代码触发change事件,可以直接调用无参数的change()方法来触发该事件。

另一种方法则是使用触发器

trigger() :

它是一个方法,用来手动触发某个事件。需要传入事件名称作为参数。input.change()相当于input.trigger('change')。

在浏览器中,有些JavaScript代码只有在用户触发下才能执行,我们称为敏感代码。

5. 动画

用JS实现动画的原理是什么呢?

原理非常简单:我们只需要以固定的时间间隔(例如,0.1秒),每次把DOM元素的CSS样式修改一点(例如,高宽各增加10%),看起来就像动画了。

但是要用JavaScript手动实现动画效果,需要编写非常复杂的代码。如果想要把动画效果用函数封装起来便于复用,那考虑的事情就更多了。jQuery的动画就是已经封装好了的常用动画方法,简单的不能再简单,只需要一行代码~

以下列举几个常用的动画方法:

show / hide

直接以无参数形式调用show()和hide(),会显示和隐藏DOM元素。但是,只要传递一个时间参数进去,就变成了动画。时间以毫秒为单位。当然也可以是'slow','fast'这些字符串。
toggle()方法则根据当前状态决定是show()还是hide()。

slideUp / slideDown

slideUp()把一个可见的DOM元素收起来,效果跟拉上窗帘似的,slideDown()相反,而slideToggle()则根据元素是否可见来决定下一步动作。

fadeIn / fadeOut

fadeIn()和fadeOut()的动画效果是淡入淡出,也就是通过不断设置DOM元素的opacity属性来实现,而fadeToggle()则根据元素是否可见来决定下一步动作

animate()

它可以实现任意动画效果,我们需要传入的参数就是DOM元素最终的CSS状态和时间,jQuery在时间段内不断调整CSS直到达到我们设定的值。animate()还可以再传入一个函数,当动画结束时,该函数将被调用。实际上这个回调函数参数对于基本动画也是适用的。

不得不提到的是,JS实现动画效果会导致代码的冗长,而且处理起来也会卡顿,在HTML5+CSS3的年代里,已经可以直接使用样式表来定义动画了。请尽可能的使用CSS3来对样式进行定义。

----------

6. AJAX

原生态的AJAX写起来冗长且要考虑到浏览器的问题,状态和错误处理写起来很麻烦。还记得jQuery的那句名言吗?——用最少的代码实现更多的功能!

jQuery对AJAX进行了一些封装,用jQuery的相关对象来处理AJAX,不但不需要考虑浏览器问题,代码也能大大简化。

6.1. ajax():

jQuery在全局对象jQuery(也就是$)绑定了ajax()函数,可以处理AJAX请求。

ajax(url, settings)函数需要接收一个URL和一个可选的settings对象,常用的选项如下:

async:是否异步执行AJAX请求,默认为true,千万不要指定为false;
method:发送的Method,缺省为'GET',可指定为'POST'、'PUT'等;
contentType:发送POST请求的格式,默认值为'application/x-www-form-urlencoded;
charset=UTF-8',也可以指定为text/plain、application/json;
data:发送的数据,可以是字符串、数组或object。如果是GET请求,data将被转换成query附加到URL上,如果是POST请求,根据contentType把data序列化成合适的格式;
headers:发送的额外的HTTP头,必须是一个object;
dataType:接收的数据格式,可以指定为'html'、'xml'、'json'、'text'等,缺省情况下根据响应的Content-Type猜测。

如何用回调函数处理返回的数据和出错时的响应呢?

jQuery的jqXHR对象类似一个Promise对象,我们可以用链式写法来处理各种回调

  1. var jqxhr = $.ajax('/api/categories', {
  2. dataType: 'json'
  3. }).done(function (data) {
  4. ajaxLog('成功, 收到的数据: ' + JSON.stringify(data));
  5. }).fail(function (xhr, status) {
  6. ajaxLog('失败: ' + xhr.status + ', 原因: ' + status);
  7. }).always(function () {
  8. ajaxLog('请求完成: 无论成功或失败都会调用');
  9. });

6.2. get():

由于GET请求最常见,所以jQuery提供了get()方法。

第一个参数是路径这点毋庸置疑。
第二个参数如果是object,jQuery自动把它变成query string然后加到URL后面。这样我们就不用关心如何用URL编码并构造一个query string了。

6.3. post():

post()和get()类似,但是传入的第二个参数默认被序列化为application/x-www-form-urlencoded,数据将不会在头部显示,而是作为POST的body被发送。

6.3. getJSON():

由于JSON用得越来越普遍,所以jQuery也提供了getJSON()方法来快速通过GET获取一个JSON对象

安全限制

jQuery的AJAX完全封装的是JavaScript的AJAX操作,所以它的安全限制和用JavaScript写AJAX完全一样。

如果需要使用JSONP,可以在ajax()中设置jsonp: 'callback',让jQuery实现JSONP跨域加载数据。

7. 扩展

$.fn对象

说白了,就是继承原始jQuery对象,为它增添新的方法。这种方式也称为编写jQuery插件。 给jQuery对象绑定一个新方法是通过扩展$.fn对象实现的。例子:

  1. $.fn.highlight1 = function () {
  2. // this已绑定为当前jQuery对象:
  3. this.css('backgroundColor', '#fffceb').css('color', '#d85030');
  4. return this;
  5. }

这里需要特别留意,扩展代码的最后一定要return this;

这里给出一个编写jQuery插件的一般步骤:

  1. .fn..defaults上;
  2. 用户在调用时可传入设定值以便覆盖默认值。

underscore

前面我们已经讲过了,JavaScript是函数式编程语言,支持高阶函数和闭包。函数式编程非常强大,可以写出非常简洁的代码。

正如jQuery统一了不同浏览器之间的DOM操作的差异,underscore则提供了一套完善的函数式编程的接口,让我们更方便地在JavaScript中实现函数式编程。

jQuery在加载时,会把自身绑定到唯一的全局变量$上,underscore与其类似,会把自身绑定到唯一的全局变量_上,笔者以为这也是为啥它的名字叫underscore的原因。

Collections

underscore为集合类对象提供了一致的接口。

集合类是指Array和Object,暂不支持Map和Set。

列举一下:

当作用于object时 当作用于array时
map/filter _.map(obj, function (value, key){}) 返回结果:array 与正常的map一样
_.mapObject 与上面类似,返回结果:object 与上面类似,返回结果:object
every / some 当集合的所有元素都满足条件时,.every()函数返回true,当集合的至少一个元素满足条件时,.some()函数返回true。使用方式与上述一致。返回结果:布尔值
max / min 这两个函数直接返回集合中最大和最小的数只作用于value,忽略掉key
groupBy 把集合的元素按照key归类,key由传入的函数返回。具体看例子
shuffle 用洗牌算法随机打乱一个集合
sample 随机选择一个或多个元素

更多完整的函数请参考underscore的文档

  1. // groupBy()
  2. var scores = [20, 81, 75, 40, 91, 59, 77, 66, 72, 88, 99];
  3. var groups = _.groupBy(scores, function (x) {
  4. if (x < 60) {
  5. return 'C';
  6. } else if (x < 80) {
  7. return 'B';
  8. } else {
  9. return 'A';
  10. }
  11. });
  12. // 结果:
  13. // {
  14. // A: [81, 91, 88, 99],
  15. // B: [75, 77, 66, 72],
  16. // C: [20, 40, 59]
  17. // }
  18. //==============================
  19. // 注意每次结果都不一样:
  20. _.shuffle([1, 2, 3, 4, 5, 6]); // [3, 5, 4, 6, 2, 1]
  21. // 注意每次结果都不一样:
  22. // 随机选1个:
  23. _.sample([1, 2, 3, 4, 5, 6]); // 2
  24. // 随机选3个:
  25. _.sample([1, 2, 3, 4, 5, 6], 3); // [6, 1, 4]

Arrays

除了集合类,数组类型的工具方法得到增强,这样使得我们可以很容易的操作Array。下面列举一些常用的:

方法名
first / last 顾名思义,这两个函数分别取第一个和最后一个元素
flatten 用于把嵌套数组转化成一位数组
zip / unzip zip()把两个或多个数组的所有元素按索引对齐,然后按索引合并成新数组。unzip()则是反过来。看实例
object 与zip的作用类似,只是最后返回结果为object类型
range 让你快速生成一个序列,不再需要用for循环实现了
  1. //zip()
  2. var names = ['Adam', 'Lisa', 'Bart'];
  3. var scores = [85, 92, 59];
  4. _.zip(names, scores);
  5. // [['Adam', 85], ['Lisa', 92], ['Bart', 59]]
  6. // 从0开始小于30,步长5
  7. _.range(0, 30, 5); // [0, 5, 10, 15, 20, 25]
  8. // 从0开始小于10:
  9. _.range(10); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

更多完整的函数请参考underscore的文档:http://underscorejs.org/#arrays

Functions

underscore本来就是为了充分发挥JavaScript的函数式编程特性,所以也提供了大量JavaScript本身没有的高阶函数。列举一些常用的:

方法名
bind 用于把对象绑定到方法的指针上。看实例
partial 为一个函数创建偏函数.这个也要看实例
once 保证某个函数执行且仅执行一次。
delay 让一个函数延迟执行,效果和setTimeout()是一样的
  1. //bind
  2. var log = console.log;
  3. log('Hello, world!');// Uncaught TypeError: Illegal invocation
  4. log.call(console, 'Hello, world!')// 输出Hello, world!
  5. var log = _.bind(console.log, console);
  6. log('Hello, world!');// 输出Hello, world!
  7. //partial
  8. //假设我们要计算xy,这时只需要调用Math.pow(x, y)就可以了。假设我们经常计算2y,每次都写Math.pow(2, y)就比较麻烦,如果创建一个新的函数能直接这样写pow2N(y)就好了,这个新函数pow2N(y)就是根据Math.pow(x, y)创建出来的偏函数,它固定住了原函数的第一个参数(始终为2)如果我们不想固定第一个参数,想固定第二个参数怎么办?可以用_作占位符,固定住第二个参数.可见,创建偏函数的目的是将原函数的某些参数固定住,可以降低新函数调用的难度。
  9. var pow2N = _.partial(Math.pow, 2);
  10. var cube = _.partial(Math.pow, _, 3);

更多完整的函数请参考underscore的文档:http://underscorejs.org/#functions

Object

和Array类似,underscore也提供了大量针对Object的函数。列举一些常用的:

方法名
keys / allKeys 可以非常方便地返回一个object自身所有的key,但不包含从原型链继承下来的,allKeys()除了object自身的key,还包含从原型链继承下来的
values 返回object自身但不包含原型链继承的所有值
extend / extendOwn 把多个object的key-value合并到第一个object并返回。如果有相同的key,后面的object的value将覆盖前面的object的value.extendOwn()和extend()类似,但获取属性时忽略从原型链继承下来的属性
delay 让一个函数延迟执行,效果和setTimeout()是一样的
clone 复制一个对象,注意,clone()是“浅复制”。所谓“浅复制”就是说,两个对象相同的key所引用的value其实是同一对象
isEqual 对两个object进行深度比较,如果内容完全相同,则返回true
  1. var a = {name: 'Bob', age: 20};
  2. _.extend(a, {age: 15}, {age: 88, city: 'Beijing'}); // {name: 'Bob', age: 88, city: 'Beijing'}
  3. // 变量a的内容也改变了:
  4. a; // {name: 'Bob', age: 88, city: 'Beijing'}

更多完整的函数请参考underscore的文档:http://underscorejs.org/#objects

Chaining

jQuery支持链式调用,underscore提供了把对象包装成能进行链式调用的方法,就是chain()函数,但要注意,因为每一步返回的都是包装对象,所以最后一步的结果需要调用value()获得最终结果。

  1. _.chain([1, 4, 9, 16, 25])
  2. .map(Math.sqrt)
  3. .filter(x => x % 2 === 1)
  4. .value();
  5. // [1, 3, 5]

Node.js

在开篇第一节的时候我们扯了一些黑暗历史,里面已经提到了node的诞生。

在Node上运行的JavaScript相比其他后端开发语言有何优势?

最大的优势是借助JavaScript天生的事件驱动机制加V8高性能引擎,使编写高性能Web服务轻而易举。

其次,JavaScript语言本身是完善的函数式语言,在前端开发时,开发人员往往写得比较随意,让人感觉JavaScript就是个“玩具语言”。但是,在Node环境下,通过模块化的JavaScript代码,加上函数式编程,并且无需考虑浏览器兼容性问题,直接使用最新的ECMAScript 6标准,可以完全满足工程上的需求。

也许你有听过io.js,这又是什么鬼?

因为Node.js是开源项目,虽然由社区推动,但幕后一直由Joyent公司资助。由于一群开发者对Joyent公司的策略不满,于2014年从Node.js项目fork出了io.js项目,决定单独发展,但两者实际上是兼容的。然而中国有句古话,叫做“分久必合,合久必分”。分家后没多久,Joyent公司表示要和解,于是,io.js项目又决定回归Node.js。

具体做法是将来io.js将首先添加新的特性,如果大家测试用得爽,就把新特性加入Node.js。io.js是“尝鲜版”,而Node.js是线上稳定版,相当于Fedora Linux和RHEL的关系。

安装Node.js

  1. 请从官网上自行下载最新的版本。node.js官方网址
  2. 在Windows上安装时务必选择全部组件,包括勾选Add to Path。
  3. 安装完成后,在Windows环境下,请打开命令提示符,然后输入node -v,如果安装正常,你会看到版本输出。
  4. 继续在命令提示符输入node,此刻你将进入Node.js的交互环境。在交互环境下,你可以输入任意JavaScript语句,例如100+200,回车后将得到输出结果。
  5. 要退出Node.js环境,连按两次Ctrl+C

npm

npm是什么东东?npm其实是Node.js的包管理工具(package manager)。

为啥我们需要一个包管理工具呢?因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。

更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。

基本上当我们安装了node以后,npm会顺带一起安装,当然如果你使用的是非常古老的版本那就不一定了。

两种运行模式

直接输入node可以进入交互模式,相当于启动了Node解释器,但是等待我们一行行的执行源代码,每输入一行就执行一行。连按两次Ctrl+C就可以退出交互模式。

另一种运行模式则是直接运行,输入 node hello.js ,相当于启动了Node解释器,然后一次性把hello.js文件的源代码给执行了,这种模式下是无法干预的。

在编写代码的时候,完全可以一边在文本编辑器里写代码,一边开一个Node交互式命令窗口,在写代码的过程中,把部分代码粘到命令行去验证,事半功倍!

module

在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Node环境中,一个.js文件就称之为一个模块(module)。

当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Node内置的模块和来自第三方的模块。 使用模块还可以避免函数名和变量名冲突。 相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

看一个例子:

  1. 'use strict';
  2. var s = 'Hello';
  3. function greet(name) {
  4. console.log(s + ', ' + name + '!');
  5. }
  6. module.exports = greet;
  7. /*上面这个语句的意思是,把函数greet作为模块的输出暴露出去,这样其他模块就可以使用greet函数了。下面再写一个文件,调用这个函数。*/
  8. var greet = require('./hello');
  9. var s = 'Michael';
  10. greet(s); // Hello, Michael!
  11. /*在上面require是node提供的一个函数。用于引入模块。请注意模块的相对路径。如果只写模块名,则Node会依次在内置模块、全局模块和当前模块下查找。*/

上面例子中的模块加载机制被称为CommonJS规范,在这个规范下,每个.js文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突,一个模块想要对外暴露变量(函数也是变量),可以用module.exports = variable;,一个模块要引用其他模块暴露的变量,用var ref = require('module_name');就拿到了引用模块的变量。

module.exports vs exports

很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量。
方法一:对module.exports赋值。
方法二:直接使用exports。

  1. module.exports = {
  2. hello: hello,
  3. greet: greet
  4. };
  5. exports.hello = hello;
  6. exports.greet = greet;
  7. // 下面是错误的写法,代码可以执行,但是模块并没有输出任何变量:
  8. exports = {
  9. hello: hello,
  10. greet: greet
  11. };

下面我们来分析一下node的加载机制:
首先,node会把整个待加载的JS文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量。函数最终返回module.exports。也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量。

如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值,如果要输出一个函数或数组,必须直接对module.exports对象赋值。所以我们可以得出结论,直接对 module.exports 赋值可以应对任何情况。

内置模块

global

Node.js是运行在服务区端的JavaScript环境,在浏览器环境下,JavaScript有且仅有一个全局对象,叫做Window对象,而在node中,也有一个唯一的全局对象,叫做global。

process

它代表当前Node.js进程。通过process对象可以拿到很多有用的信息。JavaScript程序是由事件驱动执行的单线程模型,Node.js也不例外。Node.js不断执行响应事件的JavaScript函数,直到没有任何响应事件的函数可以执行时,Node.js就退出了。如果我们想要在下一次事件响应中执行代码,可以调用process.nextTick()

  1. process.nextTick(function () {
  2. console.log('nextTick callback!');
  3. });
  4. console.log('nextTick was set!');
  5. /*nextTick was set!
  6. nextTick callback!*/

有很多JavaScript代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序本身需要判断自己到底是在什么环境下执行的,常用的方式就是根据浏览器和Node环境提供的全局变量名称来判断

  1. if (typeof(window) === 'undefined') {
  2. console.log('node.js');
  3. } else {
  4. console.log('browser');
  5. }

fs

Node.js内置的fs模块就是文件系统模块,负责读写文件。它同时提供了异步和同步的方法。

  1. 异步读取文件: fs.readFile()
  2. 同步读取文件: fs.readFileSync()不接收回调函数,函数直接返回结果。
  3. 异步写文件: fs.writeFile()。
  4. 同步写文件: fs.writeFileSync()。
  5. 异步获取文件信息: fs.stat()
  6. 同步获取文件信息: fs.statSync()

由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,因为JavaScript只有一个执行线程。

stream && pipe

stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。
pipe使得我们可以将两个流串联起来。这两个对象的应用常常会出现在一些配置文件中。

HTTP

Node.js自带的http模块完成一些从头处理TCP连接等等一些列工作。同时,它提供了request和response两个对象。

request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息。
response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

另外,解析URL需要用到Node.js提供的url模块,它使用起来非常简单,通过parse()将一个字符串解析为一个Url对象。

处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录。

至此,我们终于感受到了web程序的气息。下面让我们来模拟写一个超级简单的应用程序。

  1. 'use strict';
  2. var
  3. fs = require('fs'),
  4. url = require('url'),
  5. path = require('path'),
  6. http = require('http');
  7. // 从命令行参数获取root目录,默认是当前目录:
  8. var root = path.resolve(process.argv[2] || '.');
  9. console.log('Static root dir: ' + root);
  10. // 创建服务器:
  11. var server = http.createServer(function (request, response) {
  12. // 获得URL的path,类似 '/css/bootstrap.css':
  13. var pathname = url.parse(request.url).pathname;
  14. // 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
  15. var filepath = path.join(root, pathname);
  16. // 获取文件状态:
  17. fs.stat(filepath, function (err, stats) {
  18. if (!err && stats.isFile()) {
  19. // 没有出错并且文件存在:
  20. console.log('200 ' + request.url);
  21. // 发送200响应:
  22. response.writeHead(200);
  23. // 将文件流导向response:
  24. fs.createReadStream(filepath).pipe(response);
  25. } else {
  26. // 出错了或者文件不存在:
  27. console.log('404 ' + request.url);
  28. // 发送404响应:
  29. response.writeHead(404);
  30. response.end('404 Not Found');
  31. }
  32. });
  33. });
  34. server.listen(8080);
  35. console.log('Server is running at http://127.0.0.1:8080/');
  36. /*
  37. 在命令行运行node file_server.js /path/to/dir,把/path/to/dir改成你本地的一个有效的目录,然后在浏览器中输入http://localhost:8080/index.html
  38. */

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注