false == 0; // true false === 0; // false
要特别注意相等运算符==。JavaScript在设计时,有两种比较运算符:
第一种是==比较,它会自动转换数据类型再比较,很多时候,会得到非常诡异的结果;
第二种是===比较,它不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。
由于JavaScript这个设计缺陷,不要使用==比较,始终坚持使用===比较。
另一个例外是NaN这个特殊的Number与所有其他值都不相等,包括它自己:
NaN === NaN; // false
唯一能判断NaN的方法是通过isNaN()函数:
isNaN(NaN); // true
JavaScript的设计者希望用null表示一个空的值,而undefined表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用null。undefined仅仅在判断函数参数是否传递的情况下有用。
JavaScript的数组可以包括任意数据类型。
[1, 2, 3.14, 'Hello', null, true];
另一种创建数组的方法是通过Array()函数实现:
new Array(1, 2, 3); // 创建了数组[1, 2, 3]
var arr = [1, 2, 3.14, 'Hello', null, true]; arr[0]; // 返回索引为0的元素,即1 arr[5]; // 返回索引为5的元素,即true arr[6]; // 索引超出了范围,返回undefined
JavaScript的对象是一组由键-值组成的无序集合,例如:
var person = { name: 'Bob', age: 20, tags: ['js', 'web', 'mobile'], city: 'Beijing', hasCar: true, zipcode: null };
JavaScript对象的键都是字符串类型,值可以是任意数据类型。
变量在JavaScript中就是用一个变量名表示,变量名是大小写英文、数字、$和_的组合,且不能用数字开头。变量名也不能是JavaScript的关键字,如if、while等。
可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,但是要注意只能用var申明一次。
JavaScript在设计之初,为了方便初学者学习,并不强制要求用var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量:
i = 10; // i现在是全局变量
在同一个页面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。
使用var申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。
为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var申明变量,未使用var申明变量就使用的,将导致运行错误。
启用strict模式的方法是在JavaScript代码的第一行写上:
'use strict';
这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。
ASCII字符可以以\x##形式的十六进制表示,例如:
'\x41'; // 完全等同于 'A'
还可以用\u####表示一个Unicode字符:
'\u4e2d\u6587'; // 完全等同于 '中文'
由于多行字符串用\n写起来比较费事,所以最新的ES6标准新增了一种多行字符串的表示方法,用反引号 ...
表示:
`这是一个 多行 字符串`;
如果有很多变量需要连接,用+号就比较麻烦。ES6新增了一种模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:
var name = '小明'; var age = 20; var message = `你好, ${name}, 你今年${age}岁了!`; alert(message);
要获取字符串某个指定位置的字符,使用类似Array的下标操作,索引号从0开始:
var s = 'Hello, world!'; s[0]; // 'H' s[6]; // ' ' s[7]; // 'w' s[12]; // '!' s[13]; // undefined 超出范围的索引不会报错,但一律返回undefined
需要特别注意的是,字符串是不可变的,如果对字符串的某个索引赋值,不会有任何错误,但是,也没有任何效果:
var s = 'Test'; s[0] = 'X'; alert(s); // s仍然为'Test'
JavaScript为字符串提供了一些常用方法,注意,调用这些方法本身不会改变原有字符串的内容,而是返回一个新字符串:toUpperCase,toLowerCase
JavaScript的Array可以包含任意数据类型,并通过索引来访问每个元素。
要取得Array的长度,直接访问length属性:
var arr = [1, 2, 3.14, 'Hello', null, true]; arr.length; // 6
请注意,直接给Array的length赋一个新的值会导致Array大小的变化:
var arr = [1, 2, 3]; arr.length; // 3 arr.length = 6; arr; // arr变为[1, 2, 3, undefined, undefined, undefined] arr.length = 2; arr; // arr变为[1, 2]
Array可以通过索引把对应的元素修改为新的值,因此,对Array的索引进行赋值会直接修改这个Array:
var arr = ['A', 'B', 'C']; arr[1] = 99; arr; // arr现在变为['A', 99, 'C']
请注意,如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化:
var arr = [1, 2, 3]; arr[5] = 'x'; arr; // arr变为[1, 2, 3, undefined, undefined, 'x']
大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的Array却不会有任何错误。在编写代码时,不建议直接修改Array的大小,访问索引时要确保索引不会越界。
数组
JavaScript的Array可以包含任意数据类型,并通过索引来访问每个元素。
要取得Array的长度,直接访问length属性:
var arr = [1, 2, 3.14, 'Hello', null, true]; arr.length; // 6 请注意,直接给Array的length赋一个新的值会导致Array大小的变化:
var arr = [1, 2, 3]; arr.length; // 3 arr.length = 6; arr; // arr变为[1, 2, 3, undefined, undefined, undefined] arr.length = 2; arr; // arr变为[1, 2] Array可以通过索引把对应的元素修改为新的值,因此,对Array的索引进行赋值会直接修改这个Array:
var arr = ['A', 'B', 'C']; arr[1] = 99; arr; // arr现在变为['A', 99, 'C'] 请注意,如果通过索引赋值时,索引超过了范围,同样会引起Array大小的变化:
var arr = [1, 2, 3]; arr[5] = 'x'; arr; // arr变为[1, 2, 3, undefined, undefined, 'x'] 大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的Array却不会有任何错误。在编写代码时,不建议直接修改Array的大小,访问索引时要确保索引不会越界。
indexOf,slice
slice()就是对应String的substring()版本,它截取Array的部分元素,然后返回一个新的Array:
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; arr.slice(0, 3); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C'] arr.slice(3); // 从索引3开始到结束: ['D', 'E', 'F', 'G']
注意到slice()的起止参数包括开始索引,不包括结束索引。
如果不给slice()传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个Array:
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; var aCopy = arr.slice(); aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G'] aCopy === arr; // false
push()向Array的末尾添加若干元素,pop()则把Array的最后一个元素删除掉:
var arr = [1, 2]; arr.push('A', 'B'); // 返回Array新的长度: 4 arr; // [1, 2, 'A', 'B'] arr.pop(); // pop()返回'B' arr; // [1, 2, 'A'] arr.pop(); arr.pop(); arr.pop(); // 连续pop 3次 arr; // [] arr.pop(); // 空数组继续pop不会报错,而是返回undefined arr; // []
如果要往Array的头部添加若干元素,使用unshift()方法,shift()方法则把Array的第一个元素删掉:
var arr = [1, 2]; arr.unshift('A', 'B'); // 返回Array新的长度: 4 arr; // ['A', 'B', 1, 2] arr.shift(); // 'A' arr; // ['B', 1, 2] arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次 arr; // [] arr.shift(); // 空数组继续shift不会报错,而是返回undefined arr; // []
sort()可以对当前Array进行排序,它会直接修改当前Array的元素位置,直接调用时,按照默认顺序排序:
var arr = ['B', 'C', 'A']; arr.sort(); arr; // ['A', 'B', 'C']
能否按照我们自己指定的顺序排序呢?完全可以,我们将在后面的函数中讲到。
reverse()把整个Array的元素给掉个个,也就是反转
splice()方法是修改Array的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素:
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle']; // 从索引2开始删除3个元素,然后再添加两个元素: arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite'] arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle'] // 只删除,不添加: arr.splice(2, 2); // ['Google', 'Facebook'] arr; // ['Microsoft', 'Apple', 'Oracle'] // 只添加,不删除: arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素 arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
concat()方法把当前的Array和另一个Array连接起来,并返回一个新的Array:
实际上,concat()方法可以接收任意个元素和Array,并且自动把Array拆开,然后全部添加到新的Array里:
var arr = ['A', 'B', 'C']; arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]
join()方法是一个非常实用的方法,它把当前Array的每个元素都用指定的字符串连接起来,然后返回连接后的字符串:
var arr = ['A', 'B', 'C', 1, 2, 3]; arr.join('-'); // 'A-B-C-1-2-3'
如果Array的元素不是字符串,将自动转换为字符串后再连接。
访问属性是通过.操作符完成的,但这要求属性名必须是一个有效的变量名。如果属性名包含特殊字符,就必须用''括起来:
var xiaohong = { name: '小红', 'middle-school': 'No.1 Middle School' };
xiaohong的属性名middle-school不是一个有效的变量,就需要用''括起来。访问这个属性也无法使用.操作符,必须用['xxx']来访问:
xiaohong['middle-school']; // 'No.1 Middle School' xiaohong['name']; // '小红' xiaohong.name; // '小红'
也可以用xiaohong['name']来访问xiaohong的name属性,不过xiaohong.name的写法更简洁。我们在编写JavaScript代码的时候,属性名尽量使用标准的变量名,这样就可以直接通过object.prop的形式访问一个属性了。
实际上JavaScript对象的所有属性都是字符串,不过属性对应的值可以是任意数据类型。
如果访问一个不存在的属性会返回什么呢?JavaScript规定,访问不存在的属性不报错,而是返回undefined。
由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性:
var xiaoming = { name: '小明' }; xiaoming.age; // undefined xiaoming.age = 18; // 新增一个age属性 xiaoming.age; // 18 delete xiaoming.age; // 删除age属性 xiaoming.age; // undefined delete xiaoming['name']; // 删除name属性 xiaoming.name; // undefined delete xiaoming.school; // 删除一个不存在的school属性也不会报错
如果我们要检测xiaoming是否拥有某一属性,可以用in操作符:
var xiaoming = { name: '小明', birth: 1990, school: 'No.1 Middle School', height: 1.70, weight: 65, score: null }; 'name' in xiaoming; // true 'grade' in xiaoming; // false
不过要小心,如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的:
'toString' in xiaoming; // true
因为toString定义在object对象中,而所有对象最终都会在原型链上指向object,所以xiaoming也拥有toString属性。
要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法:
var xiaoming = { name: '小明' }; xiaoming.hasOwnProperty('name'); // true xiaoming.hasOwnProperty('toString'); // false
JavaScript把null、undefined、0、NaN和空字符串''视为false,其他值一概视为true。
for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来,要过滤掉对象继承的属性,用hasOwnProperty()来实现。
var o = { name: 'Jack', age: 20, city: 'Beijing' }; for (var key in o) { alert(key); // 'name', 'age', 'city' }
由于Array也是对象,而它的每个元素的索引被视为对象的属性,因此,for ... in循环可以直接循环出Array的索引:
var a = ['A', 'B', 'C']; for (var i in a) { alert(i); // '0', '1', '2' alert(a[i]); // 'A', 'B', 'C' }
请注意,for ... in对Array的循环得到的是String而不是Number。
JavaScript的默认对象表示方式{}可以视为其他语言中的Map或Dictionary的数据结构,即一组键值对。
但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。
为了解决这个问题,最新的ES6规范引入了新的数据类型Map。
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]); m.get('Michael'); // 95
初始化Map需要一个二维数组,或者直接初始化一个空Map。Map具有以下方法:
var m = new Map(); // 空Map m.set('Adam', 67); // 添加新的key-value m.set('Bob', 59); m.has('Adam'); // 是否存在key 'Adam': true m.get('Adam'); // 67 m.delete('Adam'); // 删除key 'Adam' m.get('Adam'); // undefined
由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉
Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。
要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:
var s1 = new Set(); // 空Set var s2 = new Set([1, 2, 3]); // 含1, 2, 3
重复元素在Set中自动被过滤:
var s = new Set([1, 2, 3, 3, '3']); s; // Set {1, 2, 3, "3"}
注意数字3和字符串'3'是不同的元素。
通过add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果,通过delete(key)方法可以删除元素。
遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。
具有iterable类型的集合可以通过新的for ... of循环来遍历。
var a = ['A', 'B', 'C']; var s = new Set(['A', 'B', 'C']); var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); for (var x of a) { // 遍历Array alert(x); } for (var x of s) { // 遍历Set alert(x); } for (var x of m) { // 遍历Map alert(x[0] + '=' + x[1]); }
for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。
当我们手动给Array对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:
var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var x in a) { alert(x); // '0', '1', '2', 'name' }
for ... in循环将把name包括在内,但Array的length属性却不包括在内。
for ... of循环则完全修复了这些问题,它只循环集合本身的元素:
var a = ['A', 'B', 'C']; a.name = 'Hello'; for (var x of a) { alert(x); // 'A', 'B', 'C' }
然而,更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。以Array为例:
var a = ['A', 'B', 'C']; a.forEach(function (element, index, array) { // element: 指向当前元素的值 // index: 指向当前索引 // array: 指向Array对象本身 alert(element); });
Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
var s = new Set(['A', 'B', 'C']); s.forEach(function (element, sameElement, set) { alert(element); });
Map的回调函数参数依次为value、key和map本身:
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); m.forEach(function (value, key, map) { alert(value); });
在JavaScript中,定义函数的方式如下:
function abs(x) { if (x >= 0) { return x; } else { return -x; } }
由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
abs(10, 'blablabla'); // 返回10 abs(-9, 'haha', 'hehe', null); // 返回9 传入的参数比定义的少也没有问题:
abs(); // 返回NaN 此时abs(x)函数的参数x将收到undefined,计算结果为NaN。
要避免收到undefined,可以对参数进行检查:
function abs(x) { if (typeof x !== 'number') { throw 'Not a number'; } if (x >= 0) { return x; } else { return -x; } }
JavaScript还有一个免费赠送的关键字arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments类似Array但它不是一个Array:
function foo(x) { alert(x); // 10 for (var i=0; i<arguments.length; i++) { alert(arguments[i]); // 10, 20, 30 } } foo(10, 20, 30);
/ foo(a[, b], c) // 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null: function foo(a, b, c) { if (arguments.length === 2) { // 实际拿到的参数是a和b,c为undefined c = b; // 把b赋给c b = null; // b变为默认值 } // ... }
由于JavaScript函数允许接收任意个参数,于是我们就不得不用arguments来获取所有参数:
function foo(a, b) { var i, rest = []; if (arguments.length > 2) { for (i = 2; i<arguments.length; i++) { rest.push(arguments[i]); } } console.log('a = ' + a); console.log('b = ' + b); console.log(rest); }
为了获取除了已定义参数a、b之外的参数,我们不得不用arguments,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest参数,有没有更好的方法?
ES6标准引入了rest参数,上面的函数可以改写为:
function foo(a, b, ...rest) { console.log('a = ' + a); console.log('b = ' + b); console.log(rest); } foo(1, 2, 3, 4, 5); // 结果: // a = 1 // b = 2 // Array [ 3, 4, 5 ] foo(1); // 结果: // a = 1 // b = undefined // Array []
rest参数只能写在最后,前面用...标识,从运行结果可知,传入的参数先绑定a、b,多余的参数以数组形式交给变量rest,所以,不再需要arguments我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。
在JavaScript中,用var申明的变量实际上是有作用域的。
如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量。
如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响。由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行。JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:
'use strict'; function foo() { var x = 'Hello, ' + y; alert(x); var y = 'Bob'; } foo();
虽然是strict模式,但语句var x = 'Hello, ' + y;并不报错,原因是变量y在稍后申明了。但是alert显示Hello, undefined,说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。
对于上述foo()函数,JavaScript引擎看到的代码相当于:
function foo() { var y; // 提升变量y的申明 var x = 'Hello, ' + y; alert(x); y = 'Bob'; }
由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量:
function foo() { var x = 1, // x初始化为1 y = x + 1, // y初始化为2 z, i; // z和i为undefined // 其他语句: for (i=0; i<100; i++) { ... } }
不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性。
由于函数定义有两种方式,以变量方式var foo = function () {}定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window对象。
alert()函数其实也是window的一个变量。
JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误。
全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。
减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。
// 唯一的全局变量MYAPP: var MYAPP = {}; // 其他变量: MYAPP.name = 'myapp'; MYAPP.version = 1.0; // 其他函数: MYAPP.foo = function () { return 'foo'; };
由于JavaScript的变量作用域实际上是函数内部,我们在for循环等语句块中是无法定义具有局部作用域的变量的:
'use strict'; function foo() { for (var i=0; i<100; i++) { // } i += 100; // 仍然可以引用变量i }
为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:
'use strict'; function foo() { var sum = 0; for (let i=0; i<100; i++) { sum += i; } i += 1; // SyntaxError }
由于var和let申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”,ES6标准引入了新的关键字const来定义常量,const与let都具有块级作用域.
var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } }; xiaoming.age; // function xiaoming.age() xiaoming.age(); // 今年调用是25,明年调用就变成26了
在一个方法内部,this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaoming的birth属性。
function getAge() { var y = new Date().getFullYear(); return y - this.birth; } var xiaoming = { name: '小明', birth: 1990, age: getAge }; xiaoming.age(); // 25, 正常结果 getAge(); // NaN
如果以对象的方法形式调用,比如xiaoming.age(),该函数的this指向被调用的对象,也就是xiaoming,这是符合我们预期的。
如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window。
var fn = xiaoming.age; // 先拿到xiaoming的age函数 fn(); // NaN
也是不行的!要保证this指向正确,必须用obj.xxx()的形式调用!
由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的this指向undefined,因此,在strict模式下,你会得到一个错误:
'use strict';
var xiaoming = { name: '小明', birth: 1990, age: function () { var y = new Date().getFullYear(); return y - this.birth; } };
var fn = xiaoming.age; fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
这个决定只是让错误及时暴露出来,并没有解决this应该指向的正确位置。
'use strict';
var xiaoming = { name: '小明', birth: 1990, age: function () { function getAgeFromBirth() { var y = new Date().getFullYear(); return y - this.birth; } return getAgeFromBirth(); } };
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
结果又报错了!原因是this指针只在age方法的函数内指向xiaoming,在函数内部定义的函数,this又指向undefined了!(在非strict模式下,它重新指向全局对象window!) 修复的办法也不是没有,我们用一个that变量首先捕获this:
'use strict';
var xiaoming = { name: '小明', birth: 1990, age: function () { var that = this; // 在方法内部一开始就捕获this function getAgeFromBirth() { var y = new Date().getFullYear(); return y - that.birth; // 用that而不是this } return getAgeFromBirth(); } };
xiaoming.age(); // 25
用var that = this;,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中 要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。 用apply修复getAge()调用:
function getAge() { var y = new Date().getFullYear(); return y - this.birth; }
var xiaoming = { name: '小明', birth: 1990, age: getAge };
xiaoming.age(); // 25 getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
另一个与apply()类似的方法是call(),唯一区别是: + apply()把参数打包成Array再传入; + call()把参数按顺序传入。 比如调用Math.max(3, 5, 4),分别用apply()和call()实现如下:
Math.max.apply(null, [3, 5, 4]); // 5 Math.max.call(null, 3, 5, 4); // 5
对普通函数调用,我们通常把this绑定为null。 JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
var count = 0; var oldParseInt = parseInt; // 保存原函数
window.parseInt = function () { count += 1; return oldParseInt.apply(null, arguments); // 调用原函数 };
// 测试: parseInt('10'); parseInt('20'); parseInt('30'); count; // 3
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。 由于map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一个新的Array作为结果:
function pow(x) { return x * x; }
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
var new_array = arr.map(function callback(currentValue, index, array) { // Return element for new_array }[, thisArg])
Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
比方说对一个Array求和,就可以用reduce实现:
var arr = [1, 3, 5, 7, 9]; arr.reduce(function (x, y) { return x + y; }); // 25
filter也是一个常用的操作,它用于把Array的某些元素过滤掉,然后返回剩下的元素。 和map()类似,Array的filter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。 例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15]; var r = arr.filter(function (x) { return x % 2 !== 0; }); r; // [1, 5, 9, 15]
把一个Array中的空字符串删掉,可以这么写:
var arr = ['A', '', 'B', null, undefined, 'C', ' ']; var r = arr.filter(function (s) { return s && s.trim(); // 注意:IE9以下的版本没有trim()方法 }); r; // ['A', 'B', 'C']
可见用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。 通常规定,对于两个元素x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。 Array的sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。
var arr = [10, 20, 1, 2]; arr.sort(function (x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; }); // [1, 2, 10, 20]
sort()方法会直接对Array进行修改,它返回的结果仍是当前Array。 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
function lazy_sum(arr) { var sum = function () { return arr.reduce(function (x, y) { return x + y; }); } return sum; }
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
调用函数f时,才真正计算求和的结果:
f(); // 15
在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,**相关参数和变量都保存在返回的函数中**,这种称为“闭包(Closure)”的程序结构拥有极大的威力。 当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:
var f1 = lazy_sum([1, 2, 3, 4, 5]); var f2 = lazy_sum([1, 2, 3, 4, 5]); f1 === f2; // false
注意到返回的函数在其定义内部引用了局部变量arr,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用了f()才执行。
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr; }
var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2];
f1(); // 16 f2(); // 16 f3(); // 16
返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了4,因此最终结果为16。 返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。 如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push((function (n) { return function () { return n * n; } })(i)); } return arr; }
var results = count(); var f1 = results[0]; var f2 = results[1]; var f3 = results[2];
f1(); // 1 f2(); // 4 f3(); // 9
'use strict';
function create_counter(initial) { var x = initial || 0; return { inc: function () { x += 1; return x; } } }
var c1 = create_counter(); c1.inc(); // 1 c1.inc(); // 2 c1.inc(); // 3
var c2 = create_counter(10); c2.inc(); // 11 c2.inc(); // 12 c2.inc(); // 13
闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。 ES6标准新增了一种新的函数:Arrow Function(箭头函数)。 为什么叫Arrow Function?因为它的定义用的就是一个箭头:
x => x * x
上面的箭头函数相当于:
function (x) { return x * x; }
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }和return.
// 两个参数: (x, y) => x * x + y * y
// 无参数: () => 3.14
// 可变参数: (x, y, ...rest) => { var i, sum = x + y; for (i=0; i<rest.length; i++) { sum += rest[i]; } return sum; }
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
// SyntaxError: x => { foo: x }
因为和函数体的{ ... }有语法冲突,所以要改为:
// ok: x => ({ foo: x })
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的this是词法作用域,由上下文确定。
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = function () { return new Date().getFullYear() - this.birth; // this指向window或undefined }; return fn(); } };
箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:
var obj = { birth: 1990, getAge: function () { var b = this.birth; // 1990 var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象 return fn(); } }; obj.getAge(); // 25
由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = { birth: 1990, getAge: function (year) { var b = this.birth; // 1990 var fn = (y) => y - this.birth; // this.birth仍是1990 return fn.call({birth:2000}, year); } }; obj.getAge(2015); // 25
generator和函数不同的是,generator由function*定义(注意多出的*号),并且,除了return语句,还可以用yield返回多次。
function* fib(max) { var t, a = 0, b = 1, n = 1; while (n < max) { yield a; t = a + b; a = b; b = t; n ++; } return a; }
直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。调用generator对象有两个方法,一是不断地调用generator对象的next()方法:
var f = fib(5); f.next(); // {value: 0, done: false} f.next(); // {value: 1, done: false} f.next(); // {value: 1, done: false} f.next(); // {value: 2, done: false} f.next(); // {value: 3, done: true}
next()方法会执行generator的代码,然后,每次遇到yield x;就返回一个对象``{value: x, done: true/false}``,然后“暂停”。返回的value就是yield的返回值,done表示这个generator是否已经执行结束了。如果done为true,则value就是return的返回值。 当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。 第二个方法是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done:
for (var x of fib(5)) { console.log(x); // 依次输出0, 1, 1, 2, 3 }
在JavaScript的世界里,一切都是对象。
typeof 123; // 'number' typeof NaN; // 'number' typeof 'str'; // 'string' typeof true; // 'boolean' typeof undefined; // 'undefined' typeof Math.abs; // 'function' typeof null; // 'object' typeof []; // 'object' typeof {}; // 'object'
可见,number、string、boolean、function和undefined有别于其他类型。特别注意null的类型是object,Array的类型也是object,如果我们用typeof将无法区分出null、Array和通常意义上的object——{}。 除了这些类型外,JavaScript还提供了包装对象,熟悉Java的小伙伴肯定很清楚int和Integer这种暧昧关系。 number、boolean和string都有包装对象。没错,在JavaScript中,字符串也区分string类型和它的包装类型。包装对象用new创建. 包装对象和原始值用===比较会返回false + 不要使用new Number()、new Boolean()、new String()创建包装对象; + 用parseInt()或parseFloat()来转换任意类型到number; + 用String()来转换任意类型到string,或者直接调用某个对象的toString()方法; + 通常不必把任意类型转换为boolean再判断,因为可以直接写if (myVar) {...}; + typeof操作符可以判断出number、boolean、string、function和undefined; + 判断Array要使用Array.isArray(arr); + 判断null请使用myVar === null; + 判断某个全局变量是否存在用typeof window.myVar === 'undefined'; + 函数内部判断某个变量是否存在用typeof myVar === 'undefined'。 任何对象都有toString()方法吗?null和undefined就没有!确实如此,这两个特殊值要除外,虽然null还伪装成了object类型。 number对象调用toString()报SyntaxError:
123.toString(); // SyntaxError
遇到这种情况,要特殊处理一下:
123..toString(); // '123', 注意是两个点! (123).toString(); // '123'
JavaScript的月份范围用整数表示是0~11,0表示一月,1表示二月……,所以要表示6月,我们传入的是5! 用\d可以匹配一个数字,\w可以匹配一个字母或数字,\s可以匹配一个空格(也包括Tab等空白符) JavaScript有两种方式创建一个正则表达式: 第一种方式是直接通过/正则表达式/写出来,第二种方式是通过new RegExp('正则表达式')创建一个RegExp对象。
var re1 = /ABC-001/; var re2 = new RegExp('ABC\-001');
re1; // /ABC-001/ re2; // /ABC-001/
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如: ``^(\d{3})-(\d{3,8})$``分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
var re = /^(\d{3})-(\d{3,8})$/; re.exec('010-12345'); // ['010-12345', '010', '12345'] re.exec('010 12345'); // null
如果正则表达式中定义了组,就可以在RegExp对象上用exec()方法提取出子串来。 exec()方法在匹配成功后,会返回一个Array,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。 exec()方法在匹配失败时返回null。 需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。
var re = /^(\d+)(0*)$/; re.exec('102300'); // ['102300', '102300', '']
由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。 必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:
var re = /^(\d+?)(0*)$/; re.exec('102300'); // ['102300', '1023', '00']
JavaScript的正则表达式还有几个特殊的标志,最常用的是g,表示全局匹配:
var r1 = /test/g; // 等价于: var r2 = new RegExp('test', 'g');
全局匹配可以多次执行exec()方法来搜索一个匹配的字符串。当我们指定g标志后,每次运行exec(),正则表达式本身会更新lastIndex属性,表示上次匹配到的最后索引:
var s = 'JavaScript, VBScript, JScript and ECMAScript'; var re=/[a-zA-Z]+Script/g;
// 使用全局匹配: re.exec(s); // ['JavaScript'] re.lastIndex; // 10
re.exec(s); // ['VBScript'] re.lastIndex; // 20
re.exec(s); // ['JScript'] re.lastIndex; // 29
re.exec(s); // ['ECMAScript'] re.lastIndex; // 44
re.exec(s); // null,直到结束仍没有匹配到
全局匹配类似搜索,因此不能使用``/^...$/``,那样只会最多匹配一次。 正则表达式还可以指定i标志,表示忽略大小写,m标志,表示执行多行匹配。
var xiaoming = { name: '小明', age: 14, gender: true, height: 1.65, grade: null, 'middle-school': '\"W3C\" Middle School', skills: ['JavaScript', 'Java', 'Python', 'Lisp'] };
JSON.stringify(xiaoming); // '{"name":"小明","age":14,"gender":true,"height":1.65,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}'
要输出得好看一些,可以加上参数,按缩进输出:
JSON.stringify(xiaoming, null, ' ');
结果:
{ "name": "小明", "age": 14, "gender": true, "height": 1.65, "grade": null, "middle-school": "\"W3C\" Middle School", "skills": [ "JavaScript", "Java", "Python", "Lisp" ] }
第二个参数用于控制如何筛选对象的键值,如果我们只想输出指定的属性,可以传入Array:
JSON.stringify(xiaoming, ['name', 'skills'], ' ');
结果:
{ "name": "小明", "skills": [ "JavaScript", "Java", "Python", "Lisp" ] }
还可以传入一个函数,这样对象的每个键值对都会被函数先处理:
function convert(key, value) { if (typeof value === 'string') { return value.toUpperCase(); } return value; }
JSON.stringify(xiaoming, convert, ' ');
上面的代码把所有属性值都变成大写:
{ "name": "小明", "age": 14, "gender": true, "height": 1.65, "grade": null, "middle-school": "\"W3C\" MIDDLE SCHOOL", "skills": [ "JAVASCRIPT", "JAVA", "PYTHON", "LISP" ] }
如果我们还想要精确控制如何序列化小明,可以给xiaoming定义一个toJSON()的方法,直接返回JSON应该序列化的数据:
var xiaoming = { name: '小明', age: 14, gender: true, height: 1.65, grade: null, 'middle-school': '\"W3C\" Middle School', skills: ['JavaScript', 'Java', 'Python', 'Lisp'], toJSON: function () { return { // 只输出name和age,并且改变了key: 'Name': this.name, 'Age': this.age }; } };
JSON.stringify(xiaoming); // '{"Name":"小明","Age":14}'
拿到一个JSON格式的字符串,我们直接用JSON.parse()把它变成一个JavaScript对象:
JSON.parse('[1,2,3,true]'); // [1, 2, 3, true] JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14} JSON.parse('true'); // true JSON.parse('123.45'); // 123.45 JSON.parse()还可以接收一个函数,用来转换解析出的属性:
JSON.parse('{"name":"小明","age":14}', function (key, value) { // 把number * 2: if (key === 'name') { return value + '同学'; } return value; }); // Object {name: '小明同学', age: 14}
JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。
var Student = { name: 'Robot', height: 1.2, run: function () { console.log(this.name + ' is running...'); } };
var xiaoming = { name: '小明' };
xiaoming.proto = Student;
JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。 ``Object.create()``方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建xiaoming:
// 原型对象: var Student = { name: 'Robot', height: 1.2, run: function () { console.log(this.name + ' is running...'); } };
function createStudent(name) { // 基于Student原型创建一个新对象: var s = Object.create(Student); // 初始化新对象: s.name = name; return s; }
var xiaoming = createStudent('小明'); xiaoming.run(); // 小明 is running... xiaoming.proto === Student; // true
JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。 当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined。
var arr = [1, 2, 3];
其原型链是: arr ----> Array.prototype ----> Object.prototype ----> null 当我们创建一个函数时:
function foo() { return 0; }
函数也是一个对象,它的原型链是: foo ----> Function.prototype ----> Object.prototype ----> null 由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法。 除了直接用{ ... }创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。它的用法是,先定义一个构造函数:
function Student(name) { this.name = name; this.hello = function () { alert('Hello, ' + this.name + '!'); } }
这确实是一个普通函数,但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象: var xiaoming = new Student('小明'); xiaoming.name; // '小明' xiaoming.hello(); // Hello, 小明! 注意,如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this;。 新创建的xiaoming的原型链是: xiaoming ----> Student.prototype ----> Object.prototype ----> null 用new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:
xiaoming.constructor === Student.prototype.constructor; // true Student.prototype.constructor === Student; // true
Object.getPrototypeOf(xiaoming) === Student.prototype; // true
xiaoming instanceof Student; // true
在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。 所以,调用构造函数千万不要忘记写new。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如jslint将可以帮你检测到漏写的new。
// PrimaryStudent构造函数: function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; }
// 空函数F: function F() { }
// 把F的原型指向Student.prototype: F.prototype = Student.prototype;
// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype: PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent: PrimaryStudent.prototype.constructor = PrimaryStudent;
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法: PrimaryStudent.prototype.getGrade = function () { return this.grade; };
// 创建xiaoming: var xiaoming = new PrimaryStudent({ name: '小明', grade: 2 }); xiaoming.name; // '小明' xiaoming.grade; // 2
// 验证原型: xiaoming.proto === PrimaryStudent.prototype; // true xiaoming.proto.proto === Student.prototype; // true
// 验证继承关系: xiaoming instanceof PrimaryStudent; // true xiaoming instanceof Student; // true
JavaScript的原型继承实现方式就是: + 定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this; + 借助中间函数F实现原型链继承,最好通过封装的inherits函数完成; + 继续在新的构造函数的原型上定义新方法。
class PrimaryStudent extends Student { constructor(name, grade) { super(name); // 记得用super调用父类的构造方法! this.grade = grade; }
myGrade() { alert('I am at grade ' + this.grade); }
}
由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的.如果引入的第三方的JavaScript中存在恶意代码,则www.foo.com网站将直接获取到www.example.com网站的用户登录信息。为了解决这个问题,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持httpOnly选项,IE从IE6 SP1开始支持。s为了确保安全,服务器端在设置Cookie时,应该始终坚持使用httpOnly。 在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。最常用的方法是``document.getElementById()``和``document.getElementsByTagName()``,以及CSS选择器``document.getElementsByClassName()``。 第二种方法是使用``querySelector()``和``querySelectorAll()``. 拿到一个DOM节点后,我们可以对它进行更新。 可以直接修改节点的文本,方法有两种: + 一种是修改innerHTML属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树. + 用innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到了,要注意对字符编码来避免XSS攻击。 + 第二种是修改innerText或textContent属性,这样可以自动对字符串进行HTML编码,保证无法设置任何HTML标签 有两个办法可以插入新的节点。一个是使用appendChild,把一个子节点添加到父节点的最后一个子节点。 如果我们要把子节点插入到指定的位置怎么办?可以使用parentElement.insertBefore(newElement, referenceElement);,子节点会插入到referenceElement之前。 要删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉 JavaScript可以以两种方式来处理表单的提交 + 方式一是通过<form>元素的submit()方法提交一个表单 + 这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击<button type="submit">时提交表单,或者用户在最后一个输入框按回车键。因此,第二种方式是响应<form>本身的onsubmit事件,在提交form时作修改
注意要return true来告诉浏览器继续提交,如果return false,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。 在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也无法获得该文件的真实路径. 随着HTML5的普及,新增的File API允许JavaScript读取文件内容,获得更多的文件信息。 HTML5的File API提供了File和FileReader两个主要对象,可以获得文件信息并读取文件。
var
fileInput = document.getElementById('test-image-file'),
info = document.getElementById('test-file-info'),
preview = document.getElementById('test-image-preview');
// 监听change事件:
fileInput.addEventListener('change', function () {
// 清除背景图片:
preview.style.backgroundImage = '';
// 检查文件是否选择:
if (!fileInput.value) {
info.innerHTML = '没有选择文件';
return;
}
// 获取File引用:
var file = fileInput.files[0];
// 获取File信息:
info.innerHTML = '文件: ' + file.name + '
' +
'大小: ' + file.size + '
' +
'修改: ' + file.lastModifiedDate;
if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
alert('不是有效的图片文件!');
return;
}
// 读取文件:
var reader = new FileReader();
reader.onload = function(e) {
var
data = e.target.result; // 'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
preview.style.backgroundImage = 'url(' + data + ')';
};
// 以DataURL的形式读取文件:
reader.readAsDataURL(file);
});
在现代浏览器上写AJAX主要依靠XMLHttpRequest对象
function success(text) { var textarea = document.getElementById('test-response-text'); textarea.value = text; }
function fail(code) { var textarea = document.getElementById('test-response-text'); textarea.value = 'Error code: ' + code; }
var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象
request.onreadystatechange = function () { // 状态发生变化时,函数被回调 if (request.readyState === 4) { // 成功完成 // 判断响应结果: if (request.status === 200) { // 成功,通过responseText拿到响应的文本: return success(request.responseText); } else { // 失败,根据响应码判断失败原因: return fail(request.status); } } else { // HTTP请求还在继续... } }
// 发送请求: request.open('GET', '/api/categories'); request.send();
alert('请求已发送,请等待响应...');
对于低版本的IE,需要换一个ActiveXObject对象 如果你想把标准写法和IE写法混在一起,可以这么写:
var request; if (window.XMLHttpRequest) { request = new XMLHttpRequest(); } else { request = new ActiveXObject('Microsoft.XMLHTTP'); }
默认情况下,JavaScript在发送AJAX请求时,URL的域名必须和当前页面完全一致。 完全一致的意思是,域名要相同(www.example.com和example.com不同),协议要相同(http和https不同),端口号要相同(默认是:80端口,它和:8080就不同)。有的浏览器口子松一点,允许端口不同,大多数浏览器都会严格遵守这个限制。 那是不是用JavaScript无法请求外域(就是其他网站)的URL了呢?方法还是有的,大概有这么几种: 一是通过Flash插件发送HTTP请求,这种方式可以绕过浏览器的安全限制,但必须安装Flash,并且跟Flash交互。不过Flash用起来麻烦,而且现在用得也越来越少了。 二是通过在同源域名下架设一个代理服务器来转发,JavaScript负责把请求发送到代理服务器: '/proxy?url=http://www.sina.com.cn' 代理服务器再把结果返回,这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。 第三种方式称为JSONP,它有个限制,只能用GET请求,并且要求返回JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用JavaScript资源:
... ...
这样一来,我们如果在页面中先准备好foo()函数,然后给页面动态加一个<script>节点,相当于动态读取外域的JavaScript资源,最后就等着接收回调了。 以163的股票查询URL为例,对于URL:http://api.money.126.net/data/feed/0000001,1399001?callback=solarex,你将得到如下返回:
solarex({"0000001":{"code": "0000001", ... });
因此我们需要首先在页面中准备好回调函数:
function solarex(data) { var p = document.getElementById('test-jsonp'); p.innerHTML = '当前价格:' + data['0000001'].name +': ' + data['0000001'].price + ';' + data['1399001'].name + ': ' + data['1399001'].price; }
最后用getPrice()函数触发:
function getPrice() { var js = document.createElement('script'), head = document.getElementsByTagName('head')[0]; js.src = 'http://api.money.126.net/data/feed/0000001,1399001?callback=solarex'; head.appendChild(js); }
如果浏览器支持HTML5,那么就可以一劳永逸地使用新的跨域策略:CORS了。 CORS全称Cross-Origin Resource Sharing,是HTML5规范定义的如何跨域访问资源。 了解CORS前,我们先搞明白概念: Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域(如sina.com)发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。 假设本域是my.com,外域是sina.com,只要响应头Access-Control-Allow-Origin为http://my.com,或者是*,本次请求就可以成功。 可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的Access-Control-Allow-Origin,决定权始终在对方手中。 上面这种跨域请求,称之为“简单请求”。简单请求包括GET、HEAD和POST(POST的Content-Type类型 仅限application/x-www-form-urlencoded、multipart/form-data和text/plain),并且不能出现任何自定义头(例如,X-Custom: 12345),通常能满足90%的需求。 对于PUT、DELETE以及其他类型如application/json的POST请求,在发送AJAX请求之前,浏览器会先发送一个OPTIONS请求(称为preflighted请求)到这个URL上,询问目标服务器是否接受:
OPTIONS /path/to/resource HTTP/1.1 Host: bar.com Origin: http://my.com Access-Control-Request-Method: POST
服务器必须响应并明确指出允许的Method:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://my.com Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS Access-Control-Max-Age: 86400
浏览器确认服务器响应的Access-Control-Allow-Methods头确实包含将要发送的AJAX请求的Method,才会继续发送AJAX,否则,抛出一个错误。 由于以POST、PUT方式传送JSON格式的数据在REST中很常见,所以要跨域正确处理POST和PUT请求,服务器端必须正确响应OPTIONS请求。 在JavaScript的世界中,所有代码都是单线程执行的。 由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现
function test(resolve, reject) { var timeOut = Math.random() * 2; log('set timeout to: ' + timeOut + ' seconds.'); setTimeout(function () { if (timeOut < 1) { log('call resolve()...'); resolve('200 OK'); } else { log('call reject()...'); reject('timeout in ' + timeOut + ' seconds.'); } }, timeOut * 1000); }
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果: var p1 = new Promise(test); var p2 = p1.then(function (result) { console.log('成功:' + result); }); var p3 = p2.catch(function (reason) { console.log('失败:' + reason); }); 变量p1是一个Promise对象,它负责执行test函数。由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:
// 如果成功,执行这个函数: p1.then(function (result) { console.log('成功:' + result); });
当test函数执行失败时,我们告诉Promise对象:
p2.catch(function (reason) { console.log('失败:' + reason); });
Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) { console.log('成功:' + result); }).catch(function (reason) { console.log('失败:' + reason); });
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。 试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); });
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) { console.log(results); // 获得一个Array: ['P1', 'P2'] });
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' });
由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。 canvas.getContext('2d')方法让我们拿到一个CanvasRenderingContext2D对象,所有的绘图操作都需要通过这个对象完成。
var ctx = canvas.getContext('2d');
如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:
gl = canvas.getContext("webgl");
$是著名的jQuery符号。实际上,jQuery把所有功能全部封装在一个全局变量jQuery中,而$也是一个合法的变量名,它是变量jQuery的别名 绝大多数时候,我们都直接用$(因为写起来更简单嘛)。但是,如果$这个变量不幸地被占用了,而且还不能改,那我们就只能让jQuery把$变量交出来,然后就只能使用jQuery这个变量:
$; // jQuery(selector, context) jQuery.noConflict(); $; // undefined jQuery; // jQuery(selector, context)
这种黑魔法的原理是jQuery在占用$之前,先在内部保存了原来的$,调用jQuery.noConflict()时会把原来保存的变量还原。 选择器是jQuery的核心。一个选择器写出来类似$('#dom-id')。 如果某个DOM节点有id属性,利用jQuery查找如下:
// 查找
按tag查找只需要写上tag名称就可以了:
var ps = $('p'); // 返回所有
节点 ps.length; // 数一数页面有多少个
节点
按class查找注意在class名称前加一个.:
var a = $('.red'); // 所有节点包含class="red"
都将返回
// 例如:
//
...
通常很多节点有多个class,我们可以查找同时包含red和green的节点:
var a = $('.red.green'); // 注意没有空格! // 符合条件的节点: //
一个DOM节点除了id和class外还可以有很多属性,很多时候按属性查找会非常方便,比如在一个表单中按属性来查找:
var email = $('[name=email]'); // 找出<??? name="email"> var passwordInput = $('[type=password]'); // 找出<??? type="password"> var a = $('[items="A B"]'); // 找出<??? items="A B">
当属性的值包含空格等特殊字符时,需要用双引号括起来。 按属性查找还可以使用前缀查找或者后缀查找:
var icons = $('[name^=icon]'); // 找出所有name属性值以icon开头的DOM // 例如: name="icon-1", name="icon-2" var names = $('[name$=with]'); // 找出所有name属性值以with结尾的DOM // 例如: name="startswith", name="endswith"
这个方法尤其适合通过class属性查找,且不受class包含多个名称的影响:
var icons = $('[class^="icon-"]'); // 找出所有class包含至少一个以icon-
开头的DOM
// 例如: class="icon-clock", class="abc icon-home"
组合查找就是把上述简单选择器组合起来使用。如果我们查找$('[name=email]'),很可能把表单外的<div name="email">也找出来,但我们只希望查找<input>,就可以这么写:
var emailInput = $('input[name=email]'); // 不会找出
同样的,根据tag和class来组合查找也很常见:
var tr = $('tr.red'); // 找出
多项选择器就是把多个选择器用,组合起来一块选:
$('p,div'); // 把
和
和
都选出来
如果两个DOM元素具有层级关系,就可以用$('ancestor descendant')来选择,层级之间用空格隔开。例如:
- JavaScript
- Python
- Lua
要选出JavaScript,可以用层级选择器:
$('ul.lang li.lang-javascript'); // [
子选择器$('parent>child')类似层级选择器,但是限定了层级关系必须是父子关系,就是<child>节点必须是<parent>节点的直属子节点。还是以上面的例子:
$('ul.lang>li.lang-javascript'); // 可以选出[
过滤器一般不单独使用,它通常附加在选择器上,帮助我们更精确地定位元素。观察过滤器的效果:
$('ul.lang li'); // 选出JavaScript、Python和Lua 3个节点
$('ul.lang li:first-child'); // 仅选出JavaScript $('ul.lang li:last-child'); // 仅选出Lua $('ul.lang li:nth-child(2)'); // 选出第N个元素,N从1开始 $('ul.lang li:nth-child(even)'); // 选出序号为偶数的元素 $('ul.lang li:nth-child(odd)'); // 选出序号为奇数的元素
// 获取超链接的jQuery对象: var a = $('#test-link'); a.on('click', function () { alert('Hello!'); });
on方法用来绑定一个事件,我们需要传入事件名称和对应的处理函数。 另一种更简化的写法是直接调用click()方法:
a.click(function () { alert('Hello!'); });
由于ready事件使用非常普遍,所以可以这样简化:
$(document).ready(function () { // on('submit', function)也可以简化: $('#testForm).submit(function () { alert('submit!'); }); });
甚至还可以再简化为:
$(function () { // init... });
上面的这种写法最为常见。如果你遇到$(function () {...})的形式,牢记这是document对象的ready事件处理函数。
// 绑定事件: a.click(function () { alert('hello!'); });
// 解除绑定: a.off('click', function () { alert('hello!'); });
直接以无参数形式调用show()和hide(),会显示和隐藏DOM元素。但是,只要传递一个时间参数进去,就变成了动画:
var div = $('#test-show-hide'); div.hide(3000); // 在3秒钟内逐渐消失
时间以毫秒为单位,但也可以是'slow','fast'这些字符串:
var div = $('#test-show-hide'); div.show('slow'); // 在0.6秒钟内逐渐显示 toggle()方法则根据当前状态决定是show()还是hide()。
slideUp()把一个可见的DOM元素收起来,效果跟拉上窗帘似的,slideDown()相反,而slideToggle()则根据元素是否可见来决定下一步动作 fadeIn()和fadeOut()的动画效果是淡入淡出,也就是通过不断设置DOM元素的opacity属性来实现,而fadeToggle()则根据元素是否可见来决定下一步动作
var div = $('#test-animate'); div.animate({ opacity: 0.25, width: '256px', height: '256px' }, 3000); // 在3秒钟内CSS过渡到设定值
animate()还可以再传入一个函数,当动画结束时,该函数将被调用:
var div = $('#test-animate'); div.animate({ opacity: 0.25, width: '256px', height: '256px' }, 3000, function () { console.log('动画已结束'); // 恢复至初始状态: $(this).css('opacity', '1.0').css('width', '128px').css('height', '128px'); });
你可能会遇到,有的动画如slideUp()根本没有效果。这是因为jQuery动画的原理是逐渐改变CSS的值,如height从100px逐渐变为0。但是很多不是block性质的DOM元素,对它们设置height根本就不起作用,所以动画也就没有效果。 此外,jQuery也没有实现对background-color的动画效果,用animate()设置background-color也没有效果。这种情况下可以使用CSS3的transition实现动画效果。 AJAX 阅读: 48909 用JavaScript写AJAX前面已经介绍过了,主要问题就是不同浏览器需要写不同代码,并且状态和错误处理写起来很麻烦。 用jQuery的相关对象来处理AJAX,不但不需要考虑浏览器问题,代码也能大大简化。 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猜测。 对常用的AJAX操作,jQuery提供了一些辅助方法。由于GET请求最常见,所以jQuery提供了get()方法,可以这么写:
var jqxhr = $.get('/path/to/resource', { name: 'Bob Lee', check: 1 });
第二个参数如果是object,jQuery自动把它变成query string然后加到URL后面,实际的URL是:
/path/to/resource?name=Bob%20Lee&check=1
这样我们就不用关心如何用URL编码并构造一个query string了。 post()和get()类似,但是传入的第二个参数默认被序列化为application/x-www-form-urlencoded:
var jqxhr = $.post('/path/to/resource', { name: 'Bob Lee', check: 1 });
实际构造的数据name=Bob%20Lee&check=1作为POST的body被发送。 由于JSON用得越来越普遍,所以jQuery也提供了getJSON()方法来快速通过GET获取一个JSON对象:
var jqxhr = $.getJSON('/path/to/resource', { name: 'Bob Lee', check: 1 }).done(function (data) { // data已经被解析为JSON对象了 });
给jQuery对象绑定一个新方法是通过扩展$.fn对象实现的。让我们来编写第一个扩展——highlight1():
$.fn.highlight1 = function () { // this已绑定为当前jQuery对象: this.css('backgroundColor', '#fffceb').css('color', '#d85030'); return this; } ```