跳转至

Javascript基本语法

基础

  • JavaScript是ECMAScript标准的一种实现,ECMAScript 6标准(简称ES6)于2015年6月正式发布。

  • <script></script>标签默认的type就是JavaScript,所以不必显式地把type指定为JavaScript。

    <script>
        //js代码
    </script>
    

  • 调试JavaScript代码,推荐Chrome浏览器

  • JavaScript不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;

让JavaScript引擎自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致,所以不要省略;

function foo() {
  return
    {name: 'foo'};
}
foo(); // undefined

// 由于JavaScript引擎在行末自动添加分号的机制,以上代码实际上变成了:
function foo() {
  return; // 自动添加了分号,相当于return undefined;
    {name: 'foo'}; // 这句代码永远不会执行到
}

// 正确的多行写法是:
function foo() {
  return { // 这里不会自动加分号,因为{表示语句尚未结束
    name: 'foo'
  }
}

  • JavaScript严格区分大小写

  • JavaScript不区分整数和浮点数,统一用Number表示

123; // 整数
0.456; // 浮点数
1.2345e3; // 科学计数法
-99; // 负数
NaN; // Not a Number,无法计算结果时用NaN表示
Infinity; // 表示无限大,当数值超过了Number所能表示的最大值时,就表示为Infinity
2 / 0; // Infinity
0 / 0; // NaN
  • 比较运算符

  • ==会自动转换数据类型再比较

  • ===不会自动转换数据类型,如果数据类型不一致返回false,如果一致再比较
  • 不要使用==比较,始终坚持使用===比较
  • NaN与所有值都不相等,包括它自己,唯一能判断NaN的方法是isNaN()函数
    NaN === NaN; // false
    isNaN(NaN); // true
    
  • JavaScript把nullundefined0NaN和空字符串''视为false,其他值一概视为true

  • 对于nullundefined,大多数情况下都应该用nullundefined仅仅在判断函数参数是否传递的情况下有用

  • 创建数组使用[]不要使用new Array()

  • strict模式

如果一个变量没有通过var声明,就自动变为全局变量;使用var声明的变量则不是全局变量,它的范围被限制在该变量被声明的函数体内。

ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制使用var声明变量,未使用var声明变量就使用将导致运行错误。

启用strict模式的方法是在JavaScript代码的第一行写上:

'use strict';
这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句,支持strict模式的浏览器将开启strict模式运行JavaScript
// 测试浏览器是否支持strict模式
'use strict';
abc = 'Hello World';
console.log(abc);

  • 字符串。ASCII字符可以用\x##形式的十六进制表示,可以用\u####表示一个Unicode字符
'\x41'; // 'A'
'\u4e2d\u6587'; // '中文'
  • 多行字符串。ES6标准新增了一种多行字符串的表示方式,用反引号`...`表示
`这是一个
多行
字符串`;
  • 模板字符串。ES6标准新增了一种模板字符串,表示方法和多行字符串一样,但会自动替换字符串中的变量
var name = '小明';
var age = 20;
var message = `你好,${name}, 你今年${age}岁了!`;
console.log(message);
  • 字符串的常见操作
var s = 'Hello';
s.length; // 5
s[0]; // 'H'
s[5]; // undefined,超出范围的索引不会报错,但一律返回undefined
s[0] = 'X'; // 字符串是不可变的,对某个索引赋值,不会有任何错误,但也没有任何效果
s.toUpperCase(); // 'HELLO'
s.toLowerCase(); // 'hello'
s.indexOf('e'); // 1
s.substring(0, 3); // 从索引0开始到3(不包括3),返回'hel'
s.substring(3); // 从索引3开始到结束,返回'lo'
  • Array的常见操作
var arr = [1, 2, 3]; // 直接给Array的length赋值会导致Array大小的变化
arr.length; // 3
arr.length = 6; // arr变为[1, 2, 3, undefined, undefined, undefined]
arr.length = 2; // arr变为[1, 2]

var arr = [1, 2, 3]; // 通过索引赋值时,索引超出了范围也会引起Array大小的变化
arr[5] = 'x'; // arr变为[1, 2, 3, undefined, undefined, 'x']
arr.indexOf(1); // 0
arr.indexOf(3); // 2
arr.indexOf('3'); // 没有找到元素时返回-1

var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; // slice()对应String的substring()方法
arr.slice(0, 3); // 从索引0开始到3结束,但不包括索引3:['A', 'B', 'C']
arr.slice(3); // 从索引3开始到结束:['D', 'E', 'F', 'G']
var aCopy = arr.slice(); // slice()不传参数会从头到尾截取所有元素,可利用这一点复制一个Array
aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G']
aCopy === arr; // false

var arr = [1, 2];
arr.push('A', 'B'); // 向Array末尾添加元素[1, 2, 'A', 'B'],返回Array新的长度:4
arr.pop(); //把最后一个元素删除掉[1, 2, 'A'],返回删除的元素:'B'
arr.pop();arr.pop();arr.pop(); // 所有元素都pop出来:[]
arr.pop(); //空数组继续pop不会报错,而是返回undefined

var arr = [1, 2];
arr.unshift('A', 'B'); //向Array头部添加元素['A', 'B', 1, 2],返回Array新的长度:4
arr.shift(); //把第一个元素删掉['B', 1, 2],返回删除的元素:'B'
arr.shift();arr.shift();arr.shift(); //所有元素都shift出来:[]
arr.shift(); //空数组继续shift不会报错,而是返回undefined

var arr = ['B', 'C', 'A'];
arr.sort(); // 排序:['A', 'B', 'C']

var arr = ['one', 'two', 'three'];
arr.reverse(); // 反转:['three', 'two', 'one']

//splice()方法是修改Array的万能方法,可以从指定索引开始删除若干个元素,然后再从该位置添加若干个元素
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
arr.splice(2, 3, 'Google', 'Facebook'); // 从索引2开始删除3个元素,然后再添加两个元素['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle'],返回删除的元素['Yahoo', 'AOL', 'Excite']
arr.splice(2, 2); // 只删除不添加['Microsoft', 'Apple', 'Oracle'],返回删除的元素['Google', 'Facebook']
arr.splice(2, 0, 'Google', 'Facebook'); // 只添加不删除['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle'],返回[],因为没有删除任何元素

var arr = ['A', 'B', 'C'];
var added = arr.concat([1, 2, 3]); // 把当前Array和另一个Array连接起来,返回一个新的Array:['A', 'B', 'C', 1, 2, 3],原arr仍为['A', 'B', 'C']
arr.concat(1, 2, [3, 4]); // concat()方法可以接收任意个元素和Array,并自动把Array拆开,然后全部添加到新的Array里:['A', 'B', 'C', 1, 2, 3, 4]

var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // join()把Array元素用指定字符串连接起来:'A-B-C-1-2-3'
  • 对象
var xiaohong = {
  name: '小红',
  'middle-school': 'No.1 Middle School'
};
xiaohong['middle-school']; // 'No.1 Middle School',如果属性名称不是有效变量名,需要用''括起来
xiaohong.age; // undefined,访问不存在的属性返回undefined
'name' in xiaohong; // true,检测对象是否拥有某一属性用in操作符
'grade' in xiaohong; // false
'toString' in xiaohong; // true,操作符in判断的属性可以是对象继承得到的
xiaohong.hasOwnProperty('name'); // true,判断属性是对象本身拥有而不是继承得到的,可以用hasOwnProperty()方法
xiaohong.hasOwnProperty('toString'); // false
  • 循环语句
// for ... in
var o = {
  name: 'Jack',
  age: 20,
  city: 'Beijing'
};
for(var key in o) {
  if(o.hasOwnProperty(key)) {
    console.log(key + '=' + o[key]);
  }
}
  • MapSetES6规范)

JavaScript对象的key必须是字符串,但实际上Number或其他数据类型也是合理的,为此ES6引入新的数据类型MapMap是一组键值对,具有极快的查找速度。

var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]); // 使用二维数组初始Map
m.get('Michael'); // 95
var m = new Map();
m.set('Adam', 67); // 使用set方法添加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
m.set('Jerry', 67);
m.set('Jerry', 88);
m.get('Jerry'); // 88,多次设置一个key的value,后设的值会覆盖前面的值

Set是一组key的集合,由于key不能重复,所以Set中没有重复key

var s1 = new Set(); // 空Set
s1.add(1);
s1.add(4); // {1, 4}
s1.add(4); // {1, 4},add(key)可以添加元素,重复添加没有效果
var s2 = new Set([1, 2, 3]); // {1,2,3}
var s = new Set([1, 2, 3, 3, '3']); // {1, 2, 3, '3'},重复元素在Set中自动被过滤
s.delete(3); // {1, 2, '3'},delete(key)可以删除元素
  • iterableES6规范)

为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。具有iterable类型的集合可以通过新的for ... of循环来遍历。

'use strict';

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
  console.log(x);
}
for(var x of s) { // 遍历Set
  console.log(x);
}
for(var x of m) { // 遍历Map
  console.log(x[0] + '=' + x[1]);
}

为什么要引入for ... of循环: 1. for ... in循环由于历史遗留问题,遍历的实际上是对象的属性名称,对于Array每个元素的索引被视为一个属性。

 当手动给Array对象添加额外的属性后,`for ... in`循环将带来意想不到的效果:
 ```javascript
 var a = ['A', 'B', 'C'];
 a.name = 'Hello';
 for(var x in a) {
   console.log(x); // '0', '1', '2', 'name'
 }
 // for ... in循环把name包括在内,但Array的length属性却不包括在内
 ```
  1. for ... of循环修复了以上问题,只循环集合本身的元素:

    var a = ['A', 'B', 'C'];
    a.name = 'Hello';
    for(var x of a) {
      console.log(x); // 'A', 'B', 'C'
    }
    

更好的方式是直接使用iterable内置的forEach()方法(ES5.1标准引入):

'use strict';

var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
  // element: 指向当前元素的值
  // index: 指向当前索引
  // array: 指向array对象本身
  console.log(element + ', index=' + index);
});

var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set){
  // Set没有索引,因此前两个参数都是元素本身
  console.log(element);
});

var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function(value, key, map){
  console.log(value);
});

在函数调用/数据构造时,将数组表达式或者string在语法层面展开;还可以在构造字面量(如[1, 2, 3]{name: 'mdn'})对象时,将对象表达式按key-value的方式展开

// 在函数调用时使用展开语法
function sum(x, y, z) {
  return x + y + z;
}
var args = [1, 2, 3];
sum(...args);
// 在new表达式中应用
var dateFields = [1970, 0, 1];
var d = new Date(...dateFields);
// 构造字面量数组时使用展开语法
var parts = ['shoulders', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes'];
// 数组拷贝
var arr = [1, 2, 3];
var arr2 = [...arr];
arr2.push(4);
arr2; // [1, 2, 3, 4]
arr; // [1, 2, 3]
// 展开语法和Object.assign()行为一致,都是浅拷贝(只遍历一层)
var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift();
a; // [[], [2], [3]]
// 连接多个数组
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
var arr3 = [...arr1, ...arr2];
// 构造字面量对象时使用展开语法
var obj1 = {foo: 'bar', x: 42};
var obj2 = {foo: 'baz', y: 13};
var clonedObj = {...obj1}; // {foo: 'bar', x: 42}
var mergedObj = {...obj1, ...obj2}; // {foo: 'baz', x: 42, y: 13}

函数

  • 函数参数

未传入的参数,函数内将收到undefined,为此可以对参数进行检查:

function abx(x) {
  if(typeof x !== 'number') {
    throw 'Not a number';
  }
  if(x >= 0) {
    return x;
  } else {
    return -x;
  }
}

JavaScript关键字arguments,只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数,类似Array但不是Array:

'use strict';

function foo(x) {
  console.log('x = ' + x); // 10
  for(var i=0; i<arguments.length; i++) {
    console.log('arg' + i + ' = ' + arguments[i]); // 10, 20, 30
  }
}
foo(10, 20, 30);

// arguments最常用于判断传入参数的个数,如下:
// 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变为默认值
  }
  // ...
}

ES6标准引入了rest参数,用于获取除了已定义参数之外的参数:

// 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]
// 如果传入的参数少于正常定义的参数,rest参数会接收一个空数组(不是undefined)
foo(1); // a = 1, b = undefined, Array[]

  • 变量作用域

var声明的变量是有作用域的,函数体内的变量作用域为整个函数体。

内部函数可以访问外部函数定义的变量,反过来则不行。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

不在任何函数内定义的变量就具有全局作用域,全局作用域的变量实际上被绑定为window对象的一个属性:

'use strict';

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

顶层函数的定义也被视为一个全局变量,并绑定到window对象:

'use strict';

function foo() {
  alert('foo');
}
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用
// 因此alert()函数也是window的一个变量
window.alert('alert');

这说明JavaScript实际上只有一个全局作用域,任何变量如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误

  • 名字空间。为了减少命名冲突,可以把自己的所有变量和函数全部绑定到一个全局变量中。

    // 唯一的全局变量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; // 仍然可以引用变量
}

为此ES6引入了新的关键字let,用let替代var可以声明一个块级作用域的变量

'use strict';

function foo() {
  var sum = 0;
  for(let i=0; i<100; i++) {
    sum += i;
  }
  i += 1; // SyntaxError
}

  • 常量。ES6标准引入新的关键字const来定义常量,constlet都具有块级作用域
'use strict';

const PI = 3.14;
PI = 3; // 某些浏览器不报错,但无效果
console.log(PI); // 3.14
  • 变量提升

JavaScript函数定义会扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部,称为变量提升

'use strict';

function foo() {
  var x = 'Hello, ' + y;
  console.log(x);
  var y = 'Bob';
}
foo(); // Hello, undefined
虽然是strict模式,但语句var x = 'Hello, ' + y;并不报错,原因是变量y在后面声明了。但console.log显示Hello, undefined,说明y的值为undefined

这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升y的赋值。JavaScript引擎看到的代码相当于:

function foo() {
  var y; // 提升变量y的声明,此时y为undefined
  var x = 'Hello, ' + y;
  console.log(x);
  y = 'Bob';
}

因此,在函数内部定义变量时,请严格遵守“在函数内部首先声明所有变量”这一规则。最常见的做法是用一个var声明函数内部用到的所有变量:

function foo() {
  var 
    x = 1, // x初始化为1
    y = x + 1, // y初始化为2
    z, i; // z和i为undefined
  // ...
  for(i=0; i<100; i++) {
    ...
  }
}

  • 解构赋值(ES6规范引入)

把一个数组的元素分别赋值给几个变量:

// 传统做法
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];

// 使用解构赋值
'use strict';

// 多个变量用[...]括起来
var [x, y, z] = ['hello', 'JavaScript', 'ES6']; // x='hello', y='JavaScript', z='ES6'
// 如果数组本身有嵌套,也可以进行解构赋值,嵌套层次和位置和保持一致
var [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
// 还可以忽略某些元素
var [, , z] = ['hello', 'JavaScript', 'ES6'];

使用解构赋值快速获取对象指定属性:

var person = {
  name: '小明',
  age: 20,
  gender: 'male',
  passport: 'G-12345678',
  school: 'No.4 middle school',
  address: {
    city: 'Beijing',
    street: 'No.1 Road',
    zipcode: '100001'
  }
};
// name, age, passport分别被赋值为对应属性
var {name, age, passport} = person;
// 对嵌套的对象属性进行赋值
// 1. 如果对应的属性不存在,变量将被赋值为undefined,因此zip=undefined
// 2. address不是变量,而是为了让city和zip获得嵌套的address对象的属性
var {name, address: {city, zip}} = person;
// 如果变量名与属性名不一致
var {name, passport:id} = person; // 这里passport不是变量,而是为了让变量id获得passport属性的值
// 解构赋值时使用默认值,避免不存在的属性返回undefined的问题
var {name, single=true} = person; // 如果person对象没有single属性,默认赋值为true

如果变量已经被声明,再次赋值时会报语法错误

var x, y;
// 解构赋值,报SyntaxError
{x, y} = {name: '小明', x: 100, y: 200};
// 解决方法是用小括号括起来
({x, y} = {name: '小明', x: 100, y: 200});

解构赋值的使用场景

// 交换两个变量x和y的值
var x=1, y=2;
[x, y] = [y, x];
// 快速获取当前页面的域名和路径
var {hostname:domain, pathname:path} = location;
// 如果一个函数接收一个对象作为参数,那可以使用解构赋值把属性绑定到变量中
// 如下:快速创建一个Date对象
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
  return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
buildDate({year:2017, month:1, day:1});
buildDate({year:2017, month:1, day:1, hour:20, minute:15});

  • this

在一个方法内部,this是一个特殊变量,它始终指向调用该方法的当前对象。单独调用函数时this指向全局对象,也就是window。要保证this指向正确必须用obj.xxx()的形式调用。

function getAge() {
  var y = new Date().getFullYear();
  return y - this.birth;
}

var xiaoming = {
  name: '小明',
  birth: 1990,
  age: getAge
};

xiaoming.age(); // 27,正常结果
getAge(); // NaN

strict模式下单独调用函数时,this指向undefined。但这只是让错误及时暴露出来。

如下,this只在age方法的函数内指向对象xiaoming,在函数内部定义的函数,this又指向了undefined(在非strict模式下,它重新指向全局对象window

'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

'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(); // 27

  • apply()

apply方法可以指定this指向哪个对象,apply方法接收两个参数,第一个是需要绑定this的变量,第二个参数是Array,表示函数本身的参数

function getAge() {
  var y = new Date().getFullYear();
  return y - this.birth;
}

var xiaoming = {
  name: '小明',
  birth: 1990,
  age: getAge
};

xiaoming.age(); // 27
getAge.apply(xiaoming, []); // 27,this指向xiaoming,参数为空

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

// 对普通函数调用,通常把this绑定为null
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

利用apply()可以动态改变函数行为。如要统计代码中parseInt()的调用次数:

'use strict';

var count = 0;
var oldParseInt = parseInt; //保存原函数

window.parseInt = function() {
  count += 1;
  return oldParseInt.apply(null, arguments); //调用原函数
};

parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3

  • map/reduce
'use strict';

function pow(x) {
  return x * x;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 把函数f(x)=x^2作用在数组的每个元素上
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
// 把数组的所有元素转为字符串
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']

Array的reduce()把一个函数作用到这个Array上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算

// 求和
var arr = [1, 3, 5, 7, 9];
arr.reduce(function(x, y) {
  return x + y;
}); // 25

// string转int
function string2int(s) {
  return s.split("").map(function(val) {
    return '0123456789'.indexOf(val);
  }).reduce(function(x, y) {
    return x * 10 + y;
  });
}
  • filter

filter用于把Array的某些元素过滤掉,然后返回剩下的元素。Array的filter接收一个函数做为参数,然后把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素

// 删除偶数,保留奇数
var arr = [1,2,3,4,5,6,7,8,9,10];
var r = arr.filter(function(x) {
  return x % 2 !== 0;
});

// 数组去重
var
  r,
  arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function(element, index, self) {
  // indexOf总是返回第一个元素的位置
  return self.indexOf(element) === index;
});

// 筛选质数
function get_primes(arr) {
  return arr.filter(function(val){
    if(val === 1) {
      return false;
    }
    var i, half = val/2;
    for(i=2; i<=half; i++) {
      if(val % i === 0) break;
    }
    return i > half;
  });
}
get_primes([1,2,3,4,5,6,7,8,9,10]);
  • sort

Array的sort()方法是用于排序的,默认把所有元素先转换为String再排序

[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

sort()方法可以接收一个比较函数来实现自定义排序。注意:sort()方法会直接对Array进行修改,返回的结果仍是当前Array

// 数组排序
var arr = [10, 20, 1, 2];
arr.sort(function(x, y) {
  if(x < y) {
    return -1;
  }
  if(x > y) {
    return 1;
  }
  return 0;
});

// 忽略大小写的排序
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function(s1, s2) {
  x1 = s1.toUpperCase();
  x2 = s2.toUpperCase();
  if(x1 < x2) {
    return -1;
  }
  if(x1 > x2) {
    return 1;
  }
  return 0;
});
  • 闭包

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

// 返回求和函数
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(); // 15
// 调用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()才执行。

// 如下,每次循环都创建一个函数,然后将3个函数添加到一个数组中返回
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()、f2()、f3()结果应该是1、4、9,但实际是:
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

闭包的作用:在Java或C++里,可以用private修饰一个成员变量,实现在对象内部封装一个私有变量。对于没有class机制,只有函数的语言里,借助闭包同样可以封装一个私有变量。

'use strict';
// 创建一个计数器
// 返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x。闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来
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

闭包还可以把多参数的函数变成单参数的函数,如:计算x^y可以用Math.pow(x, y),不过考虑到经常计算x^2或x^3,可以利用闭包创建新的函数pow2和pow3

'use strict';

function make_pow(n) {
  return function(x) {
    return Math.pow(x, n);
  }
}
// 创建两个新函数
var pow2 = make_pow(2);
var pow3 = make_pow(3);
console.log(pow2(7)); // 49
console.log(pow3(5)); // 125
  • 箭头函数(ES6规范)

ES6标准新增了一种新的函数:Arrow Function(箭头函数)

// 箭头函数的定义用的就是一个箭头
x => x * x
// 上面的箭头函数相当于
function (x) {
  return x * x;
}
// 箭头函数相当于匿名函数,并且简化了函数定义。还有一种包含多条语句
x => {
  if(x > 0) {
    return x * x;
  } else {
    return - x * x;
  }
}

// 如果参数不是一个,就需要用括号()括起来

// 两个参数
(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;
}
// 如果要返回一个对象,就要注意,如果是单表达式,如下写法会报错
x => { foo: x } // SyntaxError
// 因为函数体的{ ... }有语法冲突,所以要改为
x => ({ foo: x }) // ok

箭头函数中的this

箭头函数看上去是匿名函数的一种简写,但实际上,两者有明显区别:箭头函数内部的this是词法作用域,由上下文确定

// 回顾前面的例子,由于JavaScript函数对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();

// 使用箭头函数,以前的这种hack写法就不再需要了
var that = this;

// 由于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(ES6规范)

ES6的generator标准借鉴了Python的概念和语法,与函数很像,定义如下

function* foo(x) {
  yield x + 1;
  yield x + 2;
  return x + 3;
}

generator与函数不同的是,generator由function*定义,并且除了return语句,还可以用yield返回多次

以斐波那契数列为例,它由01开头:0 1 1 2 3 5 8 13 21 34 ...,要编写一个产生斐波那契数列的函数,可以这么写:

function fib(max) {
  var
    t,
    a = 0,
    b = 1,
    arr = [0, 1];
  while(arr.length < max) {
    [a, b] = [b, a+b];
    arr.push(b);
  }
  return arr;
}

fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函数只能返回一次,所以必须返回一个Array,但是generator可以一次返回一个数,不断返回多次:

function* fib(max) {
  var
    t,
    a = 0,
    b = 1,
    n = 0;
  while(n < max) {
    yield a;
    [a, b] = [b, a+b];
    n++;
  }
  return;
}

fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它

调用generator对象有两个方法:

  1. 不断地调用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: false}
    f.next(); // {value: undefined, done: true} 
    

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

    如果done为true,则value就是return的返回值。这时generator对象就已经全部执行完毕了,不要再继续调用next()了。

  2. for ... of循环迭代generator对象,这种方法不需要自己判断done

    for( var x of fib(10)) {
      console.log(x); // 0, 1, 1, 2, 3, ...
    }
    

generator可以在执行过程中多次返回,看上去像一个可以记住执行状态的函数,利用这一点就可以实现需要用面向对象才能实现的功能。如,用一个对象来保存状态,就得这么写:

var fib = {
  a: 0,
  b: 1,
  n: 0,
  max: 5,
  next: function() {
    var
      r = this.a,
      t = this.a + this.b;
    this.a = this.b;
    this.b = t;
    if(this.n < this.max) {
      this.n++;
      return r;
    } else {
      return undefined;
    }
  }
}

使用generator实现一个计数器

'use strict';

function* next_id() {
  var x = 1;
  while(true) yield x++;
}

var g = next_id();
g.next(); // 1
g.next(); // 2

标准对象

  1. 不要使用new Number()new Booleannew String创建包装对象
    // number, boolean, string都有包装对象
    var n = new Number(123);
    var b = new Boolean(true);
    var s = new String('str');
    // 包装对象的类型是object,与原始值用===比较会返回false
    typeof new Number(123); // 'object'
    new Number(123) === 123; // false
    typeof new Boolean(true); // 'object'
    new Boolean(true) === true; // false
    typeof new String('str'); // 'object'
    new String('str') === 'str'; // false
    // 如果不写new,Number()、Boolean()和String()被当做普通函数,把任何类型的数据转换为number、boolean和string类型(注意不是其包装类型):
    typeof Number('123'); // 'number', 相当于parseInt()或parseFloat()
    typeof Boolean('true'); // 'boolean' true
    var b1 = Boolean('false'); // true
    var b2 = Boolean(''); // false
    typeof String(123.45); // 'string'
    
  2. parseIntparseFloat来转换任意类型到number
  3. String()来转换任意类型到string,或者直接调用某个对象的toString()方法
    • 不是任何对象都有toString()方法,nullundefined就没有
    • number对象调用toString()会报语法错误
      123.toString(); // SyntaxError
      // 此时需要特殊处理一下
      123..toString(); // '123', 注意是两个点
      (123).toString(); // '123'
      
  4. 通常不必把任意类型转换为boolean再判断,因为可以直接写if (myVar) {...}
  5. typeof操作符可以判断出numberbooleanstringfunctionundefined
    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'
    
  6. 判断Array要使用Array.isArray(arr)
  7. 判断null请使用myVar === null
  8. 判断某个全局变量是否存在用typeof window.myVar === 'undefined'
  9. 函数内部判断某个变量是否存在用typeof myVar === 'undefined'

  10. Date

// 获取系统当前时间(本机操作系统时间)
var now = new Date();
now; // Thu Jan 10 2019 16:34:07 GMT+0800 (China Standard Time)
now.getFullYear(); // 2019, 年份
now.getMonth(); // 0, 月份0~11, 0表示1月
now.getDate(); // 10, 表示10号
now.getDay(); // 4, 表示星期四
now.getHours(); // 16, 24小时制
now.getMinutes(); // 34, 分钟
now.getSeconds(); // 7, 秒
now.getMilliseconds(); // 522, 毫秒数
now.getTime(); // 1547109247522, 以number形式表示的时间戳

// 创建一个指定日期和时间的Date对象
var d = new Date(2015, 5, 19, 20, 15, 30, 123);
d; // Fri Jun 19 2015 20:15:30 GMT+0800 (China Standard Time)

// 通过解析一个符合ISO 8601格式的字符串来创建Date对象
var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875
// 不过它返回的是一个时间戳,可以很容易地转换为一个Date
var d = new Date(1435146562875);
d; // Wed Jun 24 2015 19:49:22 GMT+0800 (China Standard Time)
d.getMonth(); // 5, 使用Date.parse()时传入的字符串使用实际月份01~12, 转换为Date对象后getMonth()为0~11

// 使用时间戳创建Date()就不需要关心时区转换的问题
var d = new Date(1435146562875);
d.toLocaleString(); // '6/24/2015, 7:49:22 PM'
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT'

// 获取当前时间戳
if(Date.now) {
  console.log(Date.now()); // 老版本IE没有now()方法
} else {
  console.log(new Date().getTime());
}
  • RegExp

JavaScript有两种方式创建一个正则表达式:

  1. 直接通过/正则表达式/写出来
  2. 通过new RegExp('正则表达式')创建RegExp对象
var re1 = /ABC\-001/;
var re2 = new RegExp('ABC\\-001'); // 注意字符串转义

// 测试给定的字符串是否匹配:test()
var re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false

// 切分字符串
'a b   c'.split(' '); // ['a', 'b', '', '', 'c']
'a b   c'.split(/\s+/); // ['a', 'b', 'c']
'a,b, c  d'.split(/[\s\,]+/); // ['a', 'b', 'c', 'd']
'a,b;; c  d'.split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']

提取子串,用()表示要提取的分组(Group)

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 = /^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$/;
re.exec('19:05:30'); // ['19:05:30', '19', '05', '30']

正则匹配默认是贪婪匹配,加个?就可以采用非贪婪匹配

// 贪婪匹配
var re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']
// 非贪婪匹配
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标志,表示执行多行匹配

  • JSON

JSON - JavaScript Object Notation

JSON中的数据类型:

  • number:即JavaScript中的number
  • boolean:即JavaScript中的truefalse
  • string:即JavaScript中的string
  • null:即JavaScript中的null
  • array:JavaScript中Array的表示方式[]
  • object:即JavaScript中的{...}表示方式

JSON字符集必须是UTF-8,字符串必须用双引号"",Object的key也必须用双引号""

序列化

'use strict';
var xiaoming = {
  name: '小明',
  age: 14,
  gender: true,
  height: 1.65,
  grade: null,
  'middle-school': '\"W3C\" Middle School',
  skills: ['JavaScript', 'Java', 'Python', 'Lisp']
};
var s = JSON.stringify(xiaoming);
console.log(s);
// {"name":"小明","age":14,"gender":true,"height":1.65,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}

// 缩进输出
s = JSON.stringify(xiaoming, null, '  ');
console.log(s);
/*
{
  "name": "小明",
  "age": 14,
  "gender": true,
  "height": 1.65,
  "grade": null,
  "middle-school": "\"W3C\" Middle School",
  "skills": [
    "JavaScript",
    "Java",
    "Python",
    "Lisp"
  ]
}
*/

// 第二个参数用于控制如何筛选对象的key,如:只输出指定的属性
s = JSON.stringify(xiaoming, ['name', 'skills'], '  ');
console.log(s);
/*
{
  "name": "小明",
  "skills": [
    "JavaScript",
    "Java",
    "Python",
    "Lisp"
  ]
}
*/
// 还可以传入一个函数,这样对象的每个key-value都会被函数先处理(递归的)
// 如:把所有属性值都变成大写
function convert(key, value) {
  if(typeof value === 'string') {
    return value.toUpperCase();
  }
  return value;
}
s = JSON.stringify(xiaoming, convert, '  ');
console.log(s);
/*
{
  "name": "小明",
  "age": 14,
  "gender": true,
  "height": 1.65,
  "grade": null,
  "middle-school": "\"W3C\" MIDDLE SCHOOL",
  "skills": [
    "JAVASCRIPT",
    "JAVA",
    "PYTHON",
    "LISP"
  ]
}
*/

// 可以定义toJSON()方法精确控制序列化
xiaoming.toJSON = function() {
  return {
    'Name': this.name,
    'Age': this.age
  }
}
s = JSON.stringify(xiaoming);
console.log(s);
// {"Name":"小明","Age":14}

反序列化

使用JSON.parse()将JSON格式的字符串变成一个JavaScript对象

JSON.parse('[1,2,3,true]'); // [1, 2, 3, true]
JSON.parse('{"name":"小明","age":14}'); // {name: "小明", age: 14}
JSON.parse('true'); // true
JSON.parse('123.45'); // 123.45

// JSON.parse()还可以接收一个函数用来转换解析出的属性
var obj = JSON.parse('{"name":"小明","age":14}', function(key, value) {
  if(key === 'name') {
    return value + '同学';
  }
  return value;
});
console.log(JSON.stringify(obj));
// {"name":"小明同学","age":14}

面向对象编程

JavaC#等面向对象语言中的类和实例的概念不同,JavaScript通过原型(prototype)来实现面向对象编程。原型是指当我们要创建xiaoming这个具体的学生时,并没有一个Student类型可用。但有一个现成的对象:

var robot = {
  name: 'Robot',
  height: 1.6,
  run: function() {
    console.log(this.name + ' is running...');
  }
};

这个robot对象有名字、身高,还会跑,有点像小明,就用它来“创建”小明吧

var Student = {
  name: 'Robot',
  height: 1.2,
  run: function() {
    console.log(this.name + ' is running...');
  }
}

var xiaoming = {
  name: '小明'
};

// 把xiaoming的原型指向对象Student
xiaoming.__proto__ = Student;
xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...

xiaoming有自己的name属性,但并没有定义run()方法,但由于是从Student继承而来,只要Studentrun()方法,xiaoming也可以调用

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已

如果把xiaoming的原型指向其他对象:

var Bird = {
  fly: function() {
    console.log(this.name + ' is flying...');
  }
};

xiaoming.__proto__ = Bird;

// 现在xiaoming无法run()了
xiaoming.fly(); // 小明 is flying...

在JavaScript代码运行时期,可以把xiaoming从Student变成Bird,或者变成任何对象

注意:不要直接用obj.__proto__去改变一个对象的原型,而且低版本的IE也无法使用__proto__Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但新对象什么属性都没有

var Student = {
  name: 'Robot',
  height: 1.2,
  run: function() {
    console.log(this.name + ' is running...');
  }
};

function createStudent(name) {
  var s = Object.create(Student);
  s.name = name;
  return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
  • 创建对象

当用obj.xxx访问一个对象属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined

// 创建一个Array对象
var arr = [1, 2, 3];
// 原型链:arr ----> Array.prototype ----> Object.prototype ----> null
// Array.prototype定义了indexOf()、shift()等方法,因此可以在所有的Array对象上直接调用这些方法

// 创建一个函数
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

如果又创建了xiaohongxiaojun,那么其原型与xiaoming是一样的

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

用一张图来表示上面的关系就是:

javascript-prototype-constructor-1

红色箭头是原型链。注意,Student.prototype指向的对象就是xiaomingxiaohong的原型对象,这个原型对象自己还有个属性constructor,指向Student函数本身。但是xiaomingxiaohong这些对象可没有prototype这个属性,不过可以用__proto__这个非标准用法来查看。现在我们就认为xiaomingxiaohong这些对象“继承”自Student

不过有个问题:

xiaoming.name; // 小明
xiaohong.name; // 小红
xiaoming.hello; // function: Student.hello()
xioahong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false

xiaomingxiaohong各自的name不同,这是对的,它们各自的hello是一个函数,但它们是两个不同的函数,虽然函数名和代码都相同。如果通过new Student创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存

要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移到xiaomingxiaohong这些对象共同的原型上就可以了,也就是Student.prototype

javascript-prototype-constructor-2

function Student(name) {
  this.name = name;
}

Student.prototype.hello = function() {
  alert('Hello, ' + this.name + '!');
}
  • 忘记写new怎么办

如果一个函数被定义为用地创建对象的构造函数,但是调用时忘记了写new会出现什么问题?

在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。所以调用构造函数千万不要忘记写new。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写

一个常用的编程模式是:

function Student(props) {
  this.name = props.name || '匿名';
  this.grade = props.grade || 1;
}

Student.prototype.hello = function() {
  alert('Hello, ' + this.name + '!');
};

//这个函数的优点:1.不需要new来调用,2.参数非常灵活
function createStudent(props) {
  return new Student(props || {});
}

// 如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值
// 由于参数是Object,无需记忆参数的顺序,如果恰好从JSON拿到了一个对象,就可以直接创建出xiaoming
var xiaoming = createStudent({
  name: '小明'
});
xiaoming.grade; // 1
  • 原型继承

Student构造函数

function Student(props) {
  this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
  alert('Hello, ' + this.name + '!');
}

Student原型链

javascript-prototype-constructor-2

现在,要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent

function PrimaryStudent(props) {
  Student.call(this, props); // 调用Student构造函数,绑定this变量
  this.grade = props.grade || 1;
}

但是,调用Student构造函数不等于继承了StudentPrimaryStudent创建的对象的原型是:

new PrimaryStudent ----> PrimaryStudent.prototype ----> Object.prototype ----> null

必须想办法把原型链修改为:

new PrimaryStudent ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null

如下的做法是不行的:

PrimaryStudent.prototype = Student.prototype;

此时,PrimaryStudentStudent共享一个原型对象,定义PrimaryStudent就没意义了

必须借助一个中间对象来实现正确的原型链,这个中间对象的原型要指向Student.prototype

// PrimaryStudent构造函数
function PrimaryStudent(props) {
  Student.call(this, props);
  this.grade = props.grade || 1;
}
// 空函数
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;
}

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-inherits

注意:函数F仅用于桥接,仅创建了一个new F()实例,而没有改变原有的Student定义的原型链。可以把继承这个动作用一个函数inherits()封装起来:

function inherits(Child, Parent) {
  var F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}

// 这个inherits()函数可以复用
function Student(props) {
  this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
  alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
  Student.call(this, props);
  this.grade = props.grade || 1;
}
inherits(PrimaryStudent, Student);
PrimaryStudent.prototype.getGrade = function() {
  return this.grade;
};

总结一下原型继承实现方式:

  • 定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this
  • 借助中间函数F实现原型链继承,最好通过封装的inherits函数完成
  • 继续在新的构造函数的原型上定义新方法

  • class继承

新的关键字classES6开始正式被引入到JavaScript中,目的就是定义类更简单

上文用函数实现的Student,用新的class关键字来编写:

class Student {
  constructor(name) {
    this.name = name;
  }
  // 注意没有function关键字
  hello() {
    alert('Hello, ' + this.name + '!');
  }
}
var xiaoming = new Student('小明');
xiaoming.hello();

class定义对象的另一个巨大的好处是继承更方便了,直接用extends来实现:

// PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student
class PrimaryStudent extends Student {
  // 子类的构造函数可能与父类不太相同,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
  // 自动获得父类的hello方法,定义新的myGrade方法
  myGrade() {
    alert('I am at grade ' + this.grade);
  }
}

class和原有的JavaScript原型继承没有任何区别,其作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。但并不是所有浏览器都支持class,如果一定要用,需要用工具把class代码转换为传统的prototype代码,如Babel

浏览器

目前主流的浏览器:

  1. IE6~11,历来对W3C标准支持最差,从IE10开始支持ES6标准
  2. Chrome,Google出品的基于Webkit内核浏览器,内置JavaScript引擎-V8,支持ES6
  3. Safari,Mac系统自带的基于Webkit内核的浏览器,从OS X 10.7 Lion自带6.1版本开始支持ES6
  4. Firefox,Mozilla自己研制的Gecko内核和JavaScript引擎OdinMonkey
  5. 移动设备Apple的Safari和Google的Chrome,均支持ES6

  6. 浏览器对象

  7. window,不但充当全局作用域,而且表示浏览器窗口

    window对象的innerWidthinnerHeight属性,可以获取浏览器窗口的内部宽度和高度;outerWidthouterHeight属性可以获取浏览器窗口的整个宽高。

  8. navigator对象表示浏览器的信息,常用属性:

    • navigator.appName:浏览器名称
    • navigator.appVersion:浏览器版本
    • navigator.language:浏览器设置的语言
    • navigator.platform:操作系统类型
    • navigator.userAgent:浏览器设定的User-Agent字符串

    注意:navigator的信息可以很容易被用户修改,所以JavaScript读取的值不一定正确。所以不要用if判断浏览器版本,正确的方法是充分利用JavaScript对不存在的属性返回undefined的特性,直接用||计算:

    // 不建议
    var width;
    if(getIEVersion(navigator.userAgent) < 9) {
      width = document.body.clientWidth;
    } else {
      width = window.innerWidth;
    }
    // 推荐
    var width = window.innerWidth || document.body.clientWidth;
    
  9. screen对象表示屏幕信息,常用属性:

    • screen.width:屏幕宽度,以像素为单位
    • screen.height:屏幕高度,以像素为单位
    • screen.colorDepth:返回颜色位数,如8、16、24
  10. location对象表示当前页面的URL信息

    如:http://www.example.com:8080/path/index.html?a=1&b=2#TOP

    location.href; // http://www.example.com:8080/path/index.html?a=1&b=2#TOP
    location.protocol; // http
    location.host; // www.example.com
    location.port; // 8080
    location.pathname; // /path/index.html
    location.search; // ?a=1&b=2
    location.hash; // #TOP
    location.assign(); // 加载一个新页面
    location.reload(); // 重新加载当前页面
    
  11. document对象表示当前页面,由于HTML以DOM形式表示为树形结构,document就是整个DOM树的根节点

    document有一个cookie属性,可以获取当前页面的cookie

    document.cookie; // v=123; remember=true; prefer=zh
    

    HTTP协议是无状态的,服务器使用Cookie来区分到底是哪个用户发过来的请求。JavaScript能够读取页面Cookie,而用户登录信息通常也存在Cookie中,这就存在巨大的安全隐患。如果页面引入的第三方JavaScript存在恶意代码,那用户登录信息将会泄漏。

    为了解决这个问题,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取,这个行为由浏览器实现,主流浏览器均支持,IE6从SP1开始支持。服务器端应该始终坚持使用httpOnly

  12. history对象保存了浏览器的历史记录。back()forward()相当于用户点击了“后退”或“前进”按钮

  13. 操作DOM

var test = document.getElementById('test');
var trs = document.getElementById('test-table').getElementsByTagName('tr');
var reds = document.getElementById('test-div').getElementsByClassName('red');
var cs = test.children; // 获取所有直属子节点
var first = test.firstElementChild;
var last = test.lastElementChild;
var q1 = document.querySelector('#q1');
var ps = q1.querySelectorAll('div.highlighted > p');
// IE<8不支持querySelector和querySelectorAll,IE8仅有限支持

// 更新DOM

var p = document.getElementById('p-id');
p.innerHTML = 'ABC';
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
p.innerText = '<script>alert("Hi");</script>'; // 自动进行HTML编码
p.textContent; // innerText不返回隐藏元素的文本,textContent返回所有文本,IE<9不支持textContent
// 修改CSS
p.style.color = '#ff0000';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

// 插入DOM

p.innerHTML = '<span>child</span>'; // 相当于插入,但会直接替换掉原来的所有子节点

// HTML结构:
// <p id="js">JavaScript</p>
// <div id="list">
//   <p id="java">Java</p>
//   <p id="python">Python</p>
//   <p id="scheme">Scheme</p>
// </div>
var js = document.getElementById('js');
var list = document.getElementById('list');
list.appendChild(js);
// HTML结构变成了:
// <div id="list">
//   <p id="java">Java</p>
//   <p id="python">Python</p>
//   <p id="scheme">Scheme</p>
//   <p id="js">JavaScript</p>
// </div>
// 因为我们插入的js节点已经存在于当前的文档树,因此这个节点首先会从原先的位置删除,再插入到新的位置

// 更多的时候会创建一个新的结点插入到指定位置
var haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);
// <div id="list">
//   <p id="java">Java</p>
//   <p id="python">Python</p>
//   <p id="scheme">Scheme</p>
//   <p id="haskell">Haskell</p>
// </div>

// 动态创建节点可以实现很多功能,如:动态创建一个style节点,然后添加到head节点末尾,就动态的给文档添加了新的CSS样式
var d = document.createElement('style');
d.setAttribute('type', 'text/css');
d.innerHTML = 'p { color: red; }';
document.getElementsByTagName('head')[0].appendChild(d);

// insertBefore
parentElement.insertBefore(newElement, referenceElement); // 子节点会插入到referenceElement之前

// 删除DOM

var self = document.getElementById('to-be-removed');
var parent = self.parentElement;
var removed = parent.removeChild(self);
removed === self; // true
  • 操作表单

  • 文本框:<input type="text">

  • 密码框:<input type="password">
  • 单选框:<input type="radio">
  • 复选框:<input type="checkbox">
  • 下拉框:<select>
  • 隐藏域:<input type="hidden">
// 获取值
// <input type="text" id="email">
var input = document.getElementById('email');
input.value; // 用户输入的值
// 以上方式可用于text、password、hidden、select
// 对于单选框和复选框,要用checked判断
// <label><input type="radio" name="weekday" id="monday" value="1">Monday</label>
// <label><input type="radio" name="weekday" id="tuesday" value="2">Tuesday</label>
var mon = document.getElementById("monday");
var tue = document getElementById("tuesday");
mon.value; // 1
tue.value; // 2
mon.checked; // true或false
tue.checked; // true或false

// 设置值
// <input type="text" id="email">
var input = document.getElementById("email");
input.value = 'test@example.com';
// 对于单选框和复选框,设置checked为true或false即可

// HTML5
// date、datetime、datetime-local、color
// <input type="date" value="2019-04-28">
// <input type="datetime-local" value="2019-04-28T19:56:04">
// <input type="color" value="#ff0000">
// 不支持HTML5的浏览器会将其做为text显示

// 提交表单的两种方式

// 1. 通过<form>元素的submit()方法提交,缺点是扰乱了浏览器对form的正常提交
// <form id="test-form">
//   <input type="text" name="test">
//   <button type="button" onclick="doSubmitForm()">Submit</button>
// </form>
function doSubmitForm() {
  var form = document.getElementById('test-form');
  // 修改form的input...
  form.submit();
}

// 2. 响应<form>的onsubmit事件
// <form id="test-form" onsubmit="return checkForm()">
//   <input type="text" name="test">
//   <button type="submit">Submit</button>
// </form>
function checkForm() {
  var form = document.getElementById('test-form');
  // 修改校验...
  return true; // true继续提交,false终止提交
}

// 检查和修改<input>时要充分利用<input type="hidden">来传递数据
// <form>
//   <input type="text" id="username" name="username">
//   <input type="password" id="input-password">
//   <input type="hidden" id="md5-password" name="password">
//   <button type="submit">Submit</button>
// </form>
function checkForm() {
  var input_pwd = document.getElementById('input-password');
  var md5_pwd = document.getElementById('md5-password');
  md5_pwd.value = toMD5(input_pwd.value);
  return true
}

// 注意到id为md5-password的<input>标记了name="password",而用户输入的id为input-password的<input>没有name属性。没有name属性的<input>的数据不会被提交
  • 操作文件

HTML表单中唯一可以上传文件的控制<input type="file">method必须为postenctype必须为multipart/form-data

<form id="test-form" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar">
</form>

只允许点击选择本地文件,对<input type="file">value赋值没有任何效果,JavaScript也无法获得该文件的真实路径

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

HTML5新增File API允许JavaScript读取文件内容

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 + '<br>' +
                   '大小:' + file.size + '<br>' +
                   '修改:' + 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/png;base64,iVBORw0KGgoAAAAN...
    preview.style.backgroundImage = 'url(' + data + ')';
  };
  // 以DataURL的形式读取文件,读到的文件是一个字符串,类似data:image/jpeg;base64,iVBOR...常用于设置图像
  reader.readAsDataURL(file);
});

// 上面的代码还演示了JavaScript一个重要特性:单线程执行模式
// 在JavaScript中执行多任务实际上都是异步调用,所以对于读取文件内容这种异步操作需要先设置一个回调函数
reader.onload = function(e) {
  // 文件读取完成后自动调用此函数...
};
  • AJAX
'use strict';
function success(text) {
  var textarea = document.getElementById('test-response-text');
  textarea.value = text;
}
function fail(code) {
  var textarea = document.getElementById('text-response-text');
  textarea.value = 'Error code: ' + code;
}

// 新建XMLHttpRequest对象
var request;
if(window.XMLHttpRequest) {
  request = new XMLHttpRequest;
} else {
  request = new ActiveXObject('Microsoft.XMLHTTP'); // 兼容低版本的IE
}

request.onreadystatechange = function() { // 状态发生变化时,函数被回调
  if(request.readyState === 4) { // 成功完成
    // 判断响应结果
    if(request.status === 200) {
      return success(request.responseText);
    } else {
      return fail(request.status);
    }
  } else {
    // HTTP请求还在继续
  }
}
// 发送请求
request.open('GET', '/api/categories'); // open()还有第3个参数指定是否异步,默认true
request.send();
alert('请求已发送,请等待响应...');

AJAX不能跨域请求,跨域请求实现方式:

  • 通过Flash插件发送HTTP请求,但必须安装Flash
  • 在同源域名下架设一个代理服务器来转发,但需要服务端额外做开发
  • JSONP,只能用GET请求,并且要求返回JavaScript。实际是利用了浏览器允许跨域引用JavaScript资源,如:返回JavaScript内容为foo('data'),我们在页面上先准备好foo()函数,然后给页面动态加个<script>节点,相当于动态读取外域的JavaScript资源,最后就等着接收回调了

HTML5则有新的跨域策略:CORS(Cross-Origin Resource Sharing)

Origin表示本域,当JavaScript向外域发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。Access-Control-Allow-Origin: *表示接受所有跨域请求

以上这种跨域称为“简单请求”:GETHEADPOST(Content-Type仅限application/x-www-form-urlencodedmultipart/form-datatext/plain),且不能出现任何自定义头(如:X-Custom: 12345)

对于PUTDELETE以及其他类型如application/json的POST请求,发送AJAX请求之前,浏览器会先送一个OPTIONS请求(称为preflignted请求)到这个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才会继续发送请求

  • Promise

“承诺将来会执行”对象在JavaScript中称为Promise对象,Promise有各种开源实现,在ES6中由浏览器直接支持

// 生成0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败
function test(resolve, reject) {
  console.log('start new Promise...');
  var timeOut = Math.random() * 2;
  console.log('set timeout to: ' + timeOut + ' seconds.');
  setTimeout(function() {
    if(timeOut < 1) {
      console.log('call resolve()...');
      resolve('200 OK');
    } else {
      console.log('call reject()...');
      reject('timeout in ' + timeOut + ' seconds.');
    }
  }, timeOut * 1000);
}

// 以上test()函数只关心自身逻辑,并不关心具体的resolve和reject函数将如何处理结果
// 有了执行函数,就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果

// Promise对象负责执行test函数
var p1 = new Promise(test);
// 如果成功,执行这个函数
var p2 = p1.then(function(result) {
  console.log('Done: ' + result);
});
// 如果失败,执行这个函数
var p3 = p2.catch(function(reason) {
  console.log('Failed: ' + reason);
});

// 简化的写法:
new Promise(test).then(function(result) {
  console.log('Done: ' + result);
}).catch(function (reason) {
  console.log('Failed: ' + reason);
});

可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰的分离。Promise还可以做更多的事情,如:执行要串行执行的异步任务:

// job1, job2, job3都是Promise对象
job1.then(job2).then(job3).catch(handleError);
// 0.5秒后返回input*input的结果
function multiply(input) {
  return new Promise(function(resolve, reject) {
    console.log('calculating ' + input + ' x ' + input + '...');
    setTimeout(resolve, 500, input * input);
  });
}
// 0.5秒后返回input+input的计算结果
function add(input) {
  return new Promise(function(resolve, reject) {
    console.log('calculating ' + input + ' + ' + input + '...');
    setTimeout(resolve, 500, input + input);
  });
}
var p = new Promise(function(resolve, reject) {
  console.log('start new Promise...');
  resolve(123);
});
p.then(multiply).then(add).then(multiply).then(add).then(function(result){
  console.log('Got value: ' + result);
});
// 将AJAX程序转换为Promise对象
'use strict';
function ajax(method, url, data) {
  var request = new XMLHttpRequest();
  return new Promise(function(resolve, reject) {
    request.onreadystatechange = function() {
      if(request.readyState === 4) {
        if(request.status === 200) {
          resolve(request.responseText);
        } else {
          reject(request.status);
        }
      }
    };
    request.open(method, url);
    request.send(data);
  });
}
var p = ajax('GET', 'https://www.liaoxuefeng.com/api/categories');
p.then(function(text) {
  console.log(text);
}).catch(function(status) {
  console.log('ERROR: ' + status);
});

除了串行执行若干异步任务外,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');
});
// 由于p1执行较快,Promise的then()将获得结果'P1',p2仍在继续执行,但结果将被丢弃
Promise.race([p1, p2]).then(function(result) {
  console.log(result); // 'P1'
});
  • asyncawait关键字(ES7)

asyncawait关键字可以轻松地把一个function变为异步模式

async function() {
  var data = await fs.read('/file1');
}
  • Canvas

HTML5新增的组件,可以用JavaScript在页面上绘制各种图表、动画。由于浏览器对HTML5标准支持不一致,通常在<canvas>内部添加一些说明性HTML代码。如果浏览器支持Canvas将忽略内部的HTML代码,如果不支持,将显示canvas内部的HTML:

<canvas id="test-stock" width="300" height="200">
  <p>Current Price: 25.51</p>
  <p>你的浏览器不支持Canvas</p>
</canvas>

使用之前,用canvas.getContext来测试浏览器是否支持Canvas:

'use strict';
var canvas = document.getElementById('test-canvas');
if(canvas.getContext) {
  console.log('你的浏览器支持Canvas!');
} else {
  console.log('你的浏览器不支持Canvas!');
}
'use strict';
var ctx = canvas.getContext('2d'); // 获得一个CanvasRenderingContext2D对象,所有绘图操作都需要通过这个对象完成
var gl = canvas.getContext('webgl'); // HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形

// Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位

// 绘制形状

ctx.clearRect(0, 0, 200, 200); // 擦除(0,0)位置大小为200x200的矩形(将该区域变为透明)
ctx.fillStyle = '#dddddd'; // 设置画笔颜色
ctx.fillRect(10, 10, 130, 130); // 把(10,10)位置大小为130x130的矩形涂色
// 利用Path绘制复杂路径
var path = new Path2D();
path.arc(75, 75, 50, 0, Math.PI*2, true);
path.moveTo(110,75);
path.arc(75, 75, 35, 0, Math.PI, false);
path.moveTo(65, 65);
path.arc(60, 65, 5, 0, Math.PI*2, true);
path.moveTo(95, 65);
path.arc(90, 65, 5, 0, Math.PI*2, true);
ctx.strokeStyle = '#0000ff';
ctx.stroke(path);

// 绘制文本

ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = '#666666';
ctx.font = '24px Arial';
ctx.fillStyle = '#333333';
ctx.fillText('带阴影的文字', 20, 40);

Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果实现非常复杂的操作,考虑以下优化方案:

  • 通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中
  • 尽量使用整数坐标而不是浮点数
  • 可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图
  • 背景图片如果不变可以直接用<img>标签并放到最底层

jQuery

jQuery帮我们做了这些事情:

  1. 消除浏览器差异:你不需要自己写冗长的代码来针对不同的浏览器来绑定事件、编写AJAX等代码
  2. 简洁的操作DOM的方法:写$('#test')肯定比document.getElementById('test')来得简洁
  3. 轻松实现动画、修改CSS等各种操作

  4. jQuery版本

两个主要版本1.x2.x,区别在于2.x移除了对古老的IE6、7、8的支持

  • attr()prop()方法

两者功能类似,都用于操作DOM节点的属性,但是HTML5规定有一种属性在DOM节点中可以没有值,只有出现与不出现两种:

<input id="test-radio" type="radio" name="test" checked value="1">
<!--等价于-->
<input id="test-radio" type="radio" name="test" checkec="checked" value="1">

attr()prop()对于属性checked处理有所不同:

var radio = $('#test-radio');
radio.attr('checked'); // 'checked'
radio.prop('checked'); // true
// prop()返回值更合理一些,不过,用is()方法判断更好
radio.is(':checked'); // true
// 类似的属性还有selected,处理时最好用is(':selected')
  • jQuery能够绑定的事件

  • 鼠标事件

    • click: 单击时触发
    • dblclick: 双击时触发
    • mouseenter: 鼠标进入时触发
    • mouseleave: 鼠标移出时触发
    • mousemove: 鼠标在DOM内移动时触发
    • hover: 鼠标进入和退出时触发两个函数
  • 键盘事件:键盘事件仅作用在当前焦点的DOM上,通常是<input><textarea>
    • keydown:键盘按下时触发
    • keyup:键盘松开时触发
    • keypress:按一次键后触发
  • 其他事件
    • focus:当DOM获得焦点时触发
    • blur:当DOM失去焦点时触发
    • change:当<input><select><textarea>的内容改变时触发
    • submit:当<form>提交时触发
    • ready:当页面被载入并且DOM树完成初始化后触发,仅作用于document对象,只触发一次,非常适合用来写初始化代码
      <html>
      <head>
        <script>
          $(document).on('ready', function() {
            $('#testForm').on('submit', function() {
              alert('submit');
            });
          });
          //或者
          $(function() {
            // ...
          });
        </script>
      </head>
      <body>
        <form id="testForm">
          ...
        </form>
      </body>
      </html>
      
  • 取消绑定:通过off('click', function)取消绑定的事件,off('click')一次性移除已绑定的所有click事件,off()一次性移除已绑定的所有类型的事件
    function hello() {
      alert('hello!');
    }
    a.click(hello); // 绑定事件
    // 10秒后解除绑定
    setTimeout(function() {
      a.off('click', hello);
    }, 10000);
    
  • 事件触发条件:用户在文本框输入时会触发change事件,但用JavaScript代码去改动文本框的值,将不会触发change事件。但有时我们希望用代码触发change事件,可以直接调用无参的change()方法:
    var input = $('#test-input');
    input.val('change it!');
    input.change(); // 触发change事件
    // 相当于
    input.trigger('change');
    
  • 浏览器的安全限制:有些JavaScript代码只有在用户触发下才能执行,如window.open()函数

  • jQuery动画

// show/hide/toggle:从左上角逐渐展开或收缩
var div = $('#test-show-hide');
div.hide(3000); // 3秒内逐渐消失
div.show('fast');
div.show('slow'); // 0.6秒内逐渐显示
div.toggle(); // 根据当前状态决定是show()还是hide()
// slideUp/slideDown/slideToggle:在垂直方向逐渐展开或收缩
var div = $('#test-slide');
div.slideUp(3000);
// fadeIn/fadeOut/fadeToggle:淡入淡出,通过不断设置opacity属性来实现
var div = $('#test-fade');
div.fadeOut('slow');

// animate(),实现任意动画效果,只需要传入DOM元素最终CSS状态和时间,jQuery在时间段内不断调整CSS直到达到设定的值
var div = $('#test-animate');
div.animate({
  opacity: 0.25,
  width: '256px',
  height: '256px'
}, 3000); // 在3秒内CSS过渡到设定值

// 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');
});

// 串行动画,通过delay()方法实现暂停
var div = $('#test-animates');
// slideDown-暂停-放大-暂停-缩小
div.slideDown(2000)
  .delay(1000)
  .animate({
    width: '256px',
    height: '256px'
  }, 2000)
  .delay(1000)
  .animate({
    width: '128px',
    height: '128px'
  }, 2000);
// 因为动画执行需要一段时间,所以jQuery必须不断返回新的Promise对象才能后续执行操作

// 有些动画,如slideUp()根据没效果,这是因为jQuery动画原理是逐渐改变CSS的值,很多不是block性质的DOM元素,对它们设置height根本不起作用,所以动画也就没有效果。jQuery也没有实现对background-color的动画效果,这种情况下可以使用CSS3的transition实现动画效果
  • AJAX

$.ajax(url, setting),常用的选项:

  • async:是否异步执行,默认为true
  • method:缺省为GET,可以指定为POSTPUT
  • contentType:发送POST请求的格式,默认值为application/x-www-form-urlencoded; charset=UTF-8,也可以指定为text/plainapplication/json
  • data:发送的数据,可以是字符串、数组或object,如果是GET请求,data将被转换成query附加到URL上,如果是POST请求,根据contentType把data序列化成合适的格式
  • headers:发送额外的HTTP头,必须是一个object
  • dataType:接收数据格式,可以指定为htmlxmljsontext等,缺省根据响应的Content-Type推断
  • jsonp:实现JSONP跨域加载数据
$.ajax('/api/categories', {
  dataType: 'json'
}).done(function(data) {
  console.log('成功:' + JSON.stirngify(data));
}).fail(function(xhr, status) {
  console.log('失败:' + xhr.status + ',原因:' + status);
}).always(function() {
  console.log('请求完成:无论成功失败都会调用');
});

// get
$.get('/path/to/resource', {
  name: 'Bob Lee',
  check: 1
});
// post
$.post('/path/to/resource', {
  name: 'Bob Lee',
  check: 1
});
// getJSON
$.getJSON('/path/to/resource', {
  name: 'Bob Lee',
  check: 1
}).done(function(data) {
  // data已经被解析为JSON对象了
});
  • jQuery扩展

给jQuery对象绑定一个新方法是通过扩展$.fn对象实现的,编写jQuery插件的原则:

  • $.fn绑定函数,实现插件的代码逻辑
  • 插件函数最后要return this;以支持支持链式调用
  • 插件函数要有默认值,绑定在$.fn.<pluginName>.defaults
  • 用户在调用时可传入设定值以便覆盖默认值
$.fn.highlight = function(options) {
  // 合并默认值和用户设定的值,$.extend(target, obj1, obj2, ...)把多个object对象的属性合并到第一个target对象中,遇到同名属性,总是使用靠后的对象的值,越往后优先级越高
  var opts = $.extend({}, $.fn.highlight.defaults, options);
  // this绑定为jQuery对象,所以函数内部代码可以正常调用所有jQuery对象的方法
  this.css('backgroundColor', opts.backgroundColor).css('color', opts.color);
  return this;
}
// 设定默认值
$.fn.highlight.defaults = {
  color: '#d85030',
  backgroundColor: '#fff8de'
}

// 使用时,只需要一次性设置默认值,然后就可以简单的调用highlight()了
$.fn.highlight.defaults.color = '#fff';
$.fn.highlight.defaults.backgroundColor = '#000';

对如下HTML结构:

<div id="tets-highlight">
  <p>如何编写<span>jQuery</span> <span>Plugin</span></p>
  <p>编写<span>jQuery</span> <span>Plugin</span>,要设置<span>默认值<span>,并允许用户修改<span>默认值</span>,或者运行时传入<span>其他值<span></p>
</div>
'use strict';
$.fn.highlight.defaults.color = '#659f13';
$.fn.highlight.defaults.backgroundColor = '#f2fae3';
$('#test-highlight p:first-child span').highlight();
$('#test-highlight p:last-child span').highlight({
  color: '#dd1144'
});

针对特定元素的扩展,如submit()只针对form,可以用filter()来过滤。如:要给指向外链的超链接加上跳转提示

$.fn.external = function() {
  // return返回的each()返回结果,支持链式调用
  return this.filter('a').each(function() {
    // 注意:each()内部的回调函数的this绑定为DOM本身
    var a = $(this);
    var url = a.attr('href');
    if(url && (url.indexOf('http://') === 0 || url.indexOf('https://') === 0)) {
      a.attr('href', '#0')
        .removeAttr('target')
        .append(' <i class="uk-icon-external-link"></i>')
        .click(function() {
          if(confirm('你确定要前往' + url + '?')) {
            window.open(url);
          }
        });
    }
  });
};

对如下HTML结构

<div id="test-external">
  <p>如何学习<a href="http://jquery.com">jQuery</a></p>
  <p>首先,你要学习<a href="/wiki/1022910821149312">JavaScript</a>,并了解基本的<a href="https://developer.mozilla.org/en-US/docs/Web/HTML">HTML</a></p>
</div>
'use strict';
$('#test-external a').external();

错误处理

'use strict';
var
  r1,
  r2,
  s = null;
try {
  r1 = s.length; // 此处应产生错误
  r2 = 100; // 该语句不会执行
} catch (e) {
  console.log('出错了:' + e);
} finally {
  console.log('finally');
}
console.log('r1 = ' + r1); // r1应为undefined
console.log('r2 = ' + r2); // r2应为undefined

JavaScript有一个标准的Error对象表示错误,从Error派生的TypeErrorReferenceError等错误对象

try {
  // ...
} catch(e) {
  if(e instanceof TypeError) {
    alert('Type error!');
  } else if(e instanceof Error) {
    alert(e.message);
  } else {
    alert('Error: ' + e);
  }
}

抛出错误,使用throw语句,JavaScript允许抛出任意对象,包括数字、字符串,但最好还是抛出一个Error对象

'use strict';
var r, n, s;
try {
  s = prompt('请输入一个数字');
  n = parseInt(s);
  if(isNaN(n)) {
    throw new Error('输入错误');
  }
  // 计算平方
  r = n * n;
  console.log(n + ' * ' + n + ' = ' + r);
} catch(e) {
  console.log('出错了:' + e);
}

如果一个函数内部发生了错误,它自身没有捕获,错误就会被抛到外层调用函数,如果外层函数也没有捕获,该错误会一直沿着函数调用链向上抛出,直到被JavaScript引擎捕获,代码终止执行

JavaScript引擎是一个事件驱动的执行引擎,代码总是以单线程执行,而回调函数的执行需要等到下一个满足条件的事件出现后,才会被执行。所以,涉及到异步代码,无法在调用时捕获,因为在捕获的当时,回调函数并未执行。类似的,我们处理一个事件时,在绑定事件的代码处,无法捕获事件处理函数的错误

// 如下代码是有问题的:
try {
  $btn.click(function () {
    var
      x = parseFloat($('#x').val()),
      y = parseFloat($('#y').val()),
      r;
    if (isNaN(x) || isNaN(y)) {
      throw new Error('输入有误');
    }
    r = x + y;
    alert('计算结果:' + r);
  });
} catch (e) {
  alert('输入有误!');
}

underscore

underscore提供了一套完善的函数式编程的接口,可以让我们方便的在低版本的浏览器,甚至Object上实现函数式编程。underscore把自身绑定到唯一的全局变量_

'use strict';
_.map([1, 2, 3], (x) => x * x);
_.map({a: 1, b: 2, c: 3}, (v, k) => k + '=' + v); // map()还可以作用于Object
  • Collections
'use strict';
var obj = {
  name: 'bob',
  school: 'No.1 middle school',
  address: 'xueyuan road'
};

// map/filter
var upper = _.map(obj, function(value, key) {
  return value.toUpperCase();
}); // 返回Array
var upper = _.mapObject(obj, function(value, key) {
  return value.toUpperCase();
}); // 返回Object

// every/some
// every:集合所有元素都满足条件时返回true
_.every([1, 4, 7, -3, -9], (x) => x > 0); // false
// some:集合至少一个元素都满足条件时返回true
_.some([1, 4, 7, -3, -9], (x) => x > 0); // true

// max/min
_.max([3, 5, 7, 9]); // 9
_.min([3, 5, 7, 9]); // 3
// 要判断集合不为空
_.max([]); // -Infinity
_.min([]); // Infinity
// 如果是Object,只作用于value,忽略掉key
_.max({a: 1, b: 2, c: 3}); // 3

// groupBy
// 把集合元素按照key归类
var scores = [20, 81, 75, 40, 91, 59, 77, 66, 72, 88, 99];
var groups = _.groupBy(scores, function(x) {
  if(x < 60) {
    return 'C';
  } else if(x < 80) {
    return 'B';
  } else {
    return 'A';
  }
});
// {"C":[20,40,59],"A":[81,91,88,99],"B":[75,77,66,72]}

// shuffle/sample
// shuffle()用洗牌算法随机打乱一个集合
_.shuffle([1, 2, 3, 4, 5, 6]);
// sample()随机选择一个或多个元素
_.sample([1, 2, 3, 4, 5, 6]); // 随机选一个
_.sample([1, 2, 3, 4, 5, 6], 3); // 随机选三个
'use strict';
_.first([2, 4, 6, 8]); // 2
_.last([2, 4, 6, 8]); // 8
_.flatten([1, [2], [3, [[4], [5]]]]); // [1, 2, 3, 4, 5]
_.zip(['Adam', 'Lisa', 'Bart'], [85, 92, 59]); // [['Adam', 85], ['Lisa', 92], ['Bart', 59]]
_.unzip([['Adam', 85], ['Lisa', 92], ['Bart', 59]]); // [['Adam', 'Lisa', 'Bart'], [85, 92, 59]]
_.object(['Adam', 'Lisa', 'Bart'], [85, 92, 59]); // {Adam: 85, Lisa: 92, Bart: 59}
_.range(10); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
_.range(1, 11); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
_.range(0, 30, 5); // [0, 5, 10, 15, 20, 25]
_.range(0, -10, -1); // [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
'use strict';

// bind
// 反例
var s = '  Hello  ';
s.trim(); // 'Hello'
var fn = s.trim;
fn(); // Uncaught TypeError: String.prototype.trim called on null or undefined
// 用bind将s绑定到fn()的this指针上
var fn = _.bind(s.trim, s);
fn(); // 'Hello'
// 当用一个变量fn指向一个对象的方法时,直接调用fn()是不行的,因为丢失了this对象的引用,用bind可以修复这个问题

// partial
// 计算x^y
Math.pow(x, y);
// 计算2^y,创建一个Math.pow的偏函数,固定住第一个参数始终为2
var pow2N = _.partial(Math.pow, 2);
pow2N(3); // 8
// 计算x^3,创建一个Math.pow的偏函数,固定住第二个参数始终为3,第一个参数用_作占位符
var cube = _.partial(Math.pow, _, 3);
cube(3); // 27

// memoize
// 如果一个函数调用开销很大,可能希望把结果缓存下来,如:计算阶乘
var factorial = _.memoize(function(n) {
  // console.log('start calculate ' + n + '!...');
  // var s = 1, i = n;
  // while(i>1) {
  //   s = s * i;
  //   i--;
  // }
  // console.log(n + '! = ' + s);
  // return s;

  // 递归调用可缓存更多的计算结果
  console.log('start calculate ' + n + '!...');
  if(n < 2) {
    return 1;
  }
  return n * factorial(n - 1);
});
// 第一次调用
factorial(10); // 3628800
// 控制台输出:
// start calculate 10!...
// 10! = 3628800
// 第二次调用
factorial(10); // 3628800
// 控制台没有输出

// once
// 保证某个函数执行且仅执行一次
var register = _.once(function() {
  alert('Register ok!');
});

// delay
// 可以让一个函数延迟执行
_.delay(alert, 2000); // 两秒后调用alert
// 如果函数有参数,也可以把参数传进去
var log = _.bind(console.log, console);
_.delay(log, 2000, 'Hello', 'world!');
'use strict';

// keys/allKeys
// keys返回一个object自身所有的key,但不包括从原型链上继承下来的
// allKeys除了object自身的key,还包括从原型链继承下来的
function Student(name, age) {
  this.name = name;
  this.age = age;
}
Student.prototype.school = 'No.1 Middle School';
var xiaoming = new Student('小明', 20);
_.keys(xiaoming); // ['name', 'age']
_.allKeys(xiaoming); // ['name', 'age', 'school']

// values
// 返回object自身但不包含原型链继承的所有值,没有allValues()
_.values(xiaoming); // ['小明', 20]

// mapObject
_.mapObject({a:1, b:2, c:3}, (v, k) => 100 + v); // {a: 101, b: 102, c: 103}

// invert
// 把object的每个key-value交换,key变成value,value变成key
_.invert({Adam: 90, Lisa: 85, Bart: 59}); // {59: 'Bart', 85: 'Lisa', 90: 'Adam'}

// extend/extendOwn
// 把多个object的key-value合并到第一个object并返回,相同的key,后面的object覆盖前面的
_.extend({name: 'Bob', age: 20}, {age: 15}, {age: 88, city: 'Beijing'}); // {name: 'Bob', age: 88, city: 'Beijing'}
// extendOwn()和extend()类似,但获取属性时忽略从原型链继承下来的属性

// clone
// 将原有对象的所有属性都复制到新的对象中,属于浅拷贝,即相同key引用的value是同一对象
var source = {
  name: '小明',
  age: 20,
  skills: ['JavaScript', 'CSS', 'HTML']
};
var copied = _.clone(source);
source.skills === copied.skills; // true

// isEqual
// 对两个object进行深度比较,如果内容完全相同,则返回true,对Array也可以比较
var o1 = {name:'Bob', skills: {Java: 90, JavaScript: 99}};
var o2 = {name:'Bob', skills: {JavaScript: 99, Java: 90}};
o1 === o2; // false
_.isEqual(o1, o2); // true
var o1 = ['Bob', {skills: ['Java', 'JavaScript']}];
var o2 = ['Bob', {skills: ['Java', 'JavaScript']}];
_.isEqual(o1, o2);
  • Chaining

underscore提供了把对象包装成能进行链式调用的方法:chain()

var r = _.chain([1, 4, 9, 16, 25])
         .map(Math.sqrt)
         .filter(x => x % 2 === 1)
         .value(); // [1, 3, 5]
// 因为每一步返回的都是包装对象,所以最后一步需要调用value()获得最终结果