Fork me on GitHub

重读《JavaScript高级程序设计》

life/learn/read/javascript/javascript_high_level

最近自己在休假,打算闭门几天将《JavaScript高级程序设计》(第3版)这本良心教材再回顾一遍。目前自己进入前端领域两年多,现在重读并记录下这本教材的“硬”知识点 😊 。

函数没有重载

ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如Java)中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数类型和数量)不同即可[p66]。ECMAScript的类型是松散形的,没有签名,所以是没有重载的。

1
2
3
4
5
6
7
8
function load(num){
return num + 100;
}
function load(num,name){
return num + 200;
}
var result = load(100); // 200
# 后面的函数声明覆盖掉前面的函数声明

基本的数据类型

基本类型值指的是简单的数据段,而引用类型指那些可能由多个值构成的对象[p68]。这里指出来的基本的数据类型是说的es5的哈:Undefined,Null,Boolean,NumberString

传递参数

ECMAScript 中所有的函数的参数都是按值传递的[p70]。也就是说,把函数外部的值复制给函数内部的参数,就是把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。下面分开例子介绍两种不同类型为什么是按值传递。

基本类型值

基本类型这个按值传递比较好理解,直接复制变量的值传递:

1
2
3
4
5
6
7
8
function addTen(num){
num += 10;
return num;
}
var count = 20;
var result = addTen(10);
console.log(count); // 20 ,没有变化哈
console.log(result); // 30

引用类型值

有些人认为引用类型的传参是按照引用来传的,那暂且认为他们的理解是正确的,那下面的示例结果怎么解析呢?

1
2
3
4
5
6
7
8
function setName(obj){
obj.name = '嘉明';
obj = new Object();
obj.name = '庞嘉明';
}
var person = new Object();
setName(person);
console.log(person.name); // '嘉明',为啥不是'庞嘉明'呢?

如果是按照引用传的话,那么新建的对象obj = new Object()应该是指向堆内容的对象啊,那么改变它本有的name属性值应该生效,然而并没有生效。所以它也是按值传递滴。

函数声明与函数表达式

解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁[p111]。解析器会率先读取函数声明,并使其执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析。

1
2
3
4
console.log(sum(10 , 10)); // 20
function sum(num1 , num2){
return num1 + num2;
}
1
2
3
4
console.log(sum(10 , 10)); //TypeError: sum is not a function
var sum = function(num1 , num2){
return num1 + num2;
}

apply和call

每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值[116]。call和apply在对象中还是挺有用处的。

apply()方法和call()方法的作用是相同的,区别在于接收参数的方式不同。

apply

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组,这里的参数数组可以是Array的实例,也可以是arguments对象(类数组对象)。

1
2
3
4
5
6
7
8
9
10
11
function sum(num1 , num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments); // 传入arguments类数组对象
}
function callSum2(num1,num2){
return sum.apply(this,[num1 , num2]); // 传入数组
}
console.log(callSum1(10 , 10)); // 20
console.log(callSum2(10 , 10)); // 20

call

call()方法接收的第一个参数和apply()方法接收的一样,变化的是其余的参数直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来。

1
2
3
4
5
6
7
function sum(num1 , num2){
return num1 + num2;
}
function callSum(num1 , num2){
return sum.call(this , sum1 , sum2);
}
console.log(callSum(10 , 10)); // 20

创建对象

虽然Object构造函数或者对象字面量都可以用来创建单个对象,但是这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。[p144]

工厂模式

工厂模式就是造一个模子产生一个个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createPerson(name , age ,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}

var person1 = createPerson('nicholas' , 29 , 'software engineer');
var person2 = createPerson('greg' , 27 , 'doctor');

工厂模式解决了创建多个相似对象的问题(解决创建对象时产生大量重复代码),但是没有解决对象识别的问题(即怎么知道一个对象的类型,是Person还是Animal啊)。

构造函数模式

下面使用构造函数创建特定类型的对象。这里是Person类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name , age , job){ // 注意构造函数的首字母为大写哦
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}

var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');

alert(person1.constructor == Person); // true 可以理解为person1的创造者是Person,也就是对象的类型Person

在创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

构造函数解决了重复实例话问题(也就是创建多个相似对象的问题)和解决了对象识别的问题。但是,像上面那样,person1和person2共有的方法,实例化的时候都创建了,这未免多余了。当然可以将共有的方法提取到外面,像这样:

1
2
3
4
5
6
7
8
9
10
11
function Person(name , age , job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');

将sayName提取出来,就成了全局的方法了,然而这里只有Person类创建对象的时候才使用到,这样就大才小用了吧,所以提取出来到全局方法这种操作不推荐。

原型模式

创建的每个函数都有一个prototype(原型)属性,这个属性就是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(){
}
Person.prototype.name = 'nicholas';
Person.prototype.age = 29;
Person.prototype.sayName = function(){
alert(this.name);
};

var person1 = new Person();
person1.sayName(); // nicholas

var person2 = new Person();
person2.sayName(); // nicholas

console.log(person1.sayName == person2.sayName); // true

可以有关系图如下:

life/learn/read/javascript/prototype_object

上面的Person.prototype不建议使用字面量来写Person.prototype={},虽让效果一样,但是这里重写了原本Person.prototype的对象,因此constructor属性会指向Ohject而不是Person。当然也是可以处理的啦,将指向指正确并指定’construtor’的枚举属性为enumerable: false

原型模式解决了函数共享的问题,但是也带了一个问题:实例化中对象的属性是独立的,而原型模式这里共享了。

组合使用构造函数模式和原型模式

创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name , age ,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['shelby' , 'court'];
}
Person.prototype.sayName = function(){
alert(this.name);
}

var person1 = new Person('nicholas' , 29 , 'software engineer');
var person2 = new Person('greg' , 27 , 'doctor');

person1.friends.push('van');
console.log(person1.friends); // 'shelby,court,van'
console.log(person2.friends); // 'shelby,court'
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true

动态原型模式

其他的OO语言,比如java,创建对象的类中是包含了自身的属性、方法和共有的属性、方法,如下小狗的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Dog{
int age;
public Dog(String name ){
this.age = age;
System.out.println('小狗的名字是: ' + name);
}
public void setAge(int age){
age = age;
}
public int getAge(){
System.out.println('小狗的年龄为: ' + age);
return age;
}

public static void main(String []args){
/* 创建对象 */
Dog dog = new Dog('tom');
/* 通过方法来设定age */
dog.setAge(2);
/* 调用另外一个方法获取age */
dog.getAge();
/* 也可以通过 对象.属性名 获取 */
System.out.println('变量值: ' + dog.age);
}
}

为了看起来是类那么一会事,动态原型模式把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name , age ,job){
// 属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person('nicholas' , 29 , 'software engineer');
friend.sayName();

寄生构造函数模式

在前面几种模式都不适应的情况下,可以用寄生构造函数模式(数据结构中就使用到哈),寄生构造函数模式可以看成是工厂模式和构造函数模式的结合体。其基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name , age , job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}

var friend = new Person('nicholas', 29 , 'software engineer');
friend.sayName(); // nicholas

关于寄生构造函数模式,需要说明:返回的对象与构造函数或者与构造函数的原型属性直接没有什么关系;也就是说,构造函数返回的对象与构造函数外部创建的对象没有什么区别。为此,不能依赖instanceof操作符来确定对象类型。由于存在上面的问题,建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

稳妥对象适合在一些安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点不同:意识新创建对象的实例方法不引用this,二是不使用new操作符调用构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name , age , job){
// 创建要返回的对象
var o = new Object();

// 可以在这里定义私有的变量和函数

// 添加方法
o.sayName = function(){
alert(name); // 不使用this.name
};

// 返回对象
return o;
}

var friend = Person('nicholas', 29 , 'software engineer'); // 不使用new
friend.sayName(); // 'nicholas'

继承

许多的OO语言都支持两种继承方法:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且实现主要是依靠原型链来实现的。[p162]

原型链

原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。回顾下构造函数、原型和实例的关系: 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subProperty = false;
}

// 继承了SuperType,重点哦
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
return this.subProperty;
}

var instance = new SubType();
console.log(instance.getSuperValue()); // true

上面代码中原型链如下:

life/learn/read/javascript/prototype_chain

原型链继承带来两个问题:一是原型实际上变成了另一个类型的实例,于是,原先的实例属性也就变成了现在原型的属性,共享了属性。二是在创建子类型的实例时,不能在没有影响所有对象实例的情况下向超类型的构造函数传递参数。

借用构造函数

借用构造函数解决原型链继承带来的不能向构造函数传递仓鼠的问题。这里使用到了apply()或者call()方法在新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType(){
this.colors = ['red','blue','green'];
}
function SubType(){
// 继承SuperType
SubType.call(this); // 使用SubType.apply(this)同效
}

var instance1 = new SubType();
instance1.color.push('black');
console.log(instance1.colors); // 'red,blue,green,black'

var instance2 = new SubType();
console.log(instance2.colors); // 'red,blue,green'

上面的例子中,我在父类型构造函数中没有传参数,看者感兴趣的话可以自己加下参数来实验一番咯。

借用构造函数解决了原型链继承的确定,但是又没有接纳原型链的优点:共享。下面的组合继承结合了原型链和借用构造函数,容纳了两者的优点。

组合继承

组合继承的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
// 继承属性
SuperType.call(this,name);
this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor =SubType; // 避免重写构造函数指向错误
SubType.prototype.sayAge = function(){
console.log(this.age);
}

var instance1 = new SubType('nicholas' , 29);
instance1.colors.push('black');
console.log(instance1.colors); // 'red,blue,green,black'
instance1.sayName(); // 'nicholas'
instance1.sayAge(); // 29

var instance2 = new SubType('greg' , 27);
console.log(instance2.colors); // 'red,blue,green'
instance2.sayName(); // 'greg'
instance2.sayAge(); // 27

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。

原型式继承

原型式继承是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function object(o){ // 传入一个对象
function F(){};
F.prototype = o;
return new F();
}

var person = {
name : 'nicholas',
friends: ['shelby','court','van']
};

var anotherPerson = object(person);
anotherPerson.name = 'greg';
anotherPerson.friends.push('rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'linda';
yetAnotherPerson.friends.push('barbie');

console.log(person.friends); // 'shelby,court,van,rob,barbie'

寄生式继承

寄生式继承是与原型继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即是创建了一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的做了所有工作一样返回对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function object(o){ // 传入一个对象
function F(){};
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log('hi');
};
return clone;
}
var person = {
name : 'nicholas',
friends : ['shelby','court','van']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'

上面的例子中,新对象anotherPerson不仅具有person的所有属性和方法,而且还有了自己的sayHi()方法。

寄生组合式继承

组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。寄生组合式继承能够解决这个问题。

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型的原型的一个副本而已。寄生组合式继承的基本模式如下:

1
2
3
4
5
6
function inheritPrototype(subType,superType){
var prototype = Object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象,防止下面重写constructor属性
subType.prototype = prototype; // 指定对象

}

一个完整的例子如下,相关插图见书[p173]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function inheritPrototype(subType,superType){
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;

}
function SuperType(name){
this.name = name;
this.color = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name, age){
SuperType.call(this,age); // 只在这调用了一次超类型的构造函数
}

inheritPrototype(SubType , SuperType);

SubType.prototype.sayAge = function(){
console.log(this.age);
}

var instance = new SubType('nicholas' , 29);

上面的例子的高效处体现在它只调用了一次SuperType构造函数,并且避免了在SubType.prototype上创建不必要的,多余的属性。与此同时,原型链还能保持不变;因此还能正常使用instanceof和inPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。我的理解是,函数内的函数使用到外层函数的变量延长变量的生存时间,造成常驻内存。例子见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
var a = 2;
return function(){
a += 1;
console.log(a);
}
}

var baz = foo();

baz(); // 3
baz(); // 4
baz(); // 5
baz(); // 6

上面的例子中,外部的函数foo()执行完成之后,正常的情况下应该销毁a变量的,但是内部的返回的匿名函数使用到该变量,不能销毁。如果需要销毁的话,可以改写成下面:

1
2
3
4
5
6
7
8
9
10
11
function foo(){
var a = 2;
return function(){
a += 1;
console.log(a);
}
}
var baz = foo();
baz(); // 3

baz = null; // 将内部的匿名函数赋值为空

从闭包说起

谈到了闭包,这让我想起了不久前刷知乎看到一篇文章。自己整理如下:

1
2
3
4
5
6
7
8
for(var i = 0 ; i < 5; i++){
setTimeout(function(){
console.log(i);
},1000)
}
console.log(i);

// 5,5,5,5,5,5

上面的代码是输出了6个5,而这6个5是这样执行的,先输出全局中的console.log(i),然后是过了1秒种后,瞬间输出了5个5(为什么用瞬间这个词呢,怕看者理解为每过一秒输出一个5)。解读上面的代码的话,可以通过狭义范围(es5)的理解:同步 => 异步 => 回调 (回调也是属于异步的范畴,所以我这里指明了狭义啦)。先是执行同步的for,遇到异步的setTimeout(setTimeout和setInterval属于异步哈)后将其放入队列中等待,接着往下执行全局的console.log(i),将其执行完成后执行异步的队列。

追问1:闭包

改写上面的代码,期望输出的结果为:5 => 0,1,2,3,4。改造的方式一:

1
2
3
4
5
6
7
8
9
10
for(var i = 0; i < 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000);
})(i);
}
console.log(i);

// 5,0,1,2,3,4

上面的代码巧妙的利用IIFE(Immediately Invoked Function Expression:声明即执行的函数表达式)来解决闭包造成的问题,闭包的解析看上面。

方法二:利用js中基本类型的参数传递是按值传递的特征,改造代码如下

1
2
3
4
5
6
7
8
9
10
11
var output = function(i){
setTimeout(function(){
console.log(i);
},1000);
};
for(var i = 0; i < 5; i++){
output(i); // 这里传过去的i值被复制了
}
console.log(i);

// 5,0,1,2,3,4

上面改造的两个方法都是执行代码后先输出5,然后过了一秒种依次输出0,1,2,3,4。

如果不要考虑全局中的console.log(i)输出的5,而是循环中输出的0,1,2,3,4。你还可以使用ES6的let块级作用域语法,实现超级简单:

1
2
3
4
5
6
7
for(let i = 0; i < 5; i++){
setTimeout(function(){
console.log(i);
},1000);
}

// 0,1,2,3,4

上面是过了一秒钟后,依次输出0,1,2,3,4。这种做法类似于无形中添加了闭包。那么,如果使用ES6语法的话,会怎样实现5,0,1,2,3,4呢?

追问2:ES6

改造刚开始的代码使得输出的结果是每隔一秒输出0,1,2,3,4,大概第五秒输出5。

在不使用ES6的情况下:

1
2
3
4
5
6
7
8
9
10
11
12
for(var i = 0; i < 5; i++){
(function(j){
setTimeout(function(){
console.log(j);
},1000*j);
})(i);
}
setTimeout(function(){
console.log(i);
},1000*i);

// 0,1,2,3,4,5

上面的代码简单粗暴,但是不推荐。看题目是每隔一秒输出一个值,再回调实现最后的5输出,这个时候应该使用ES6语法来考虑,应该使用Promise方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const tasks = [];
for(var i = 0; i < 5; i++){// 这里的i声明不能改成let,改成let的话请看下一段代码
((j)=>{
tasks.push(new Promise((resolve)=>{ // 执行tasks
setTimeout(()=>{
console.log(j);
resolve(); // 这里一定要resolve,否则代码不会按照预期执行
},1000*j);
}))
})(i);
}

Promise.all(tasks).then(()=>{ // 执行完tasks,回调
setTimeout(()=>{
console.log(i);
},1000);
});

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5

如果是使用let,我的改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const tasks = [];
for (let i = 0; i < 5; i++) {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve();
}, 1000 * i);
}));
}

Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(tasks.length);
}, 1000);
});

// 0,1,2,3,4,5

上面的代码比较庞杂,可以将其颗粒话,模块化。对上面两段代码的带var那段进行改造后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const tasks = []; // 这里存放异步操作的Promise
const output = (i) => new Promise((resolve) => {
setTimeout(()=>{
console.log(i);
},1000*i);
});

// 生成全部的异步操作
for(var i = 0; i < 5; i++){
tasks.push(output(i));
}
// 异步操作完成之后,输出最后的i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
},1000);
});

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5

追问3:ES7

既然ES6的Promise可以写,那么ES7是否可以写呢,从而让代码更加简洁易读?那就使用到到了异步操作的async await特性啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 模拟其他语言中的sleep,实际上可以是任何异步操作
const sleep = (time) => new Promise((resolve) => {
setTimeout(resolve , time);
});

(async () => {
for(var i = 0; i < 5; i++){
await sleep(1000);
console.log(i);
}

await sleep(1000);
console.log(i);
})();

// 符合要求的每隔一秒输出
// 0,1,2,3,4,5

浏览器窗口位置

IE、Safari、Opera和Chrome都提供了screenLeft和screenTop属性,分别表示浏览器窗口相对于屏幕左上角和上边的位置[p197]。Firefox则以screenX和screenY属性来表示。为了兼容各个浏览器,可以入下面这样写:

1
2
var leftPos = (typeof window.screenLeft == "number")?window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number")? window.screenTop : window.screenY;

浏览器窗口大小

由于浏览器厂商以及历史的问题,无法确认浏览器本身的大小,但是可以取得视口的大小[p198]。如下:

1
2
3
4
5
6
7
8
9
10
11
12
var pageWidth = window.innerWidth,
pageHeight = window.innerHeight;

if(typeof pageWidth != "number"){
if(document.compatMode == 'CSS1Compat'){ // 标准模式下的低版本ie
pageWidth = document.documentElement.clientWidth;
pageHeight = document.documentElement.clientHeight;
}else{ // 混杂模式下的chrome
pageWidth = document.body.clientWidth;
pageHeight = document.body.clientHeight;
}
}

上面的示例可以简写成下面这样:

1
2
var pageWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
var pageHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;

canvas中的变换

为绘制上下文应用变换,会导致使用不同的变换矩阵应用处理,从而产生不同的结果。[p453]

可通过下面的方法来修改变换矩阵:

  • rotation(angle):围绕原点旋转图像angle弧度
  • scale(scaleX,scaleY)
  • translate(x,y): 将坐标原点移动到(x,y)。执行这个变换后,坐标(0,0)会变成之前由(x,y)表示的点。

JSON

关于JSON,最重要的是要理解它是一种数据格式,不是一种编程语言。

对象字面量和JSON格式比较

先来看下对象字面量demo写法:

1
2
3
4
5
6
7
8
9
10
var person = {
name : "nicholas",
age : 29
};

# 上面的代码也可以写成下面的
var person = {
"name" : "nicholas",
"age" : 29
};

而上面的对象写成数据的话,就是下面这样了:

1
2
3
4
5
6
{
"name": "nicholas ",
"age": 29
}

# 可到网站 https://www.bejson.com/ 验证

⚠️ 与JavaScript对象字面量相比,JSON对象又两个地方不一样。首先,没有声明变量(JSON中没有变量的概念)。其次,没有分号(因为这不是JavaScript语句,所以不需要分号)。留意的是,对象的属性必须加双引号(不是单引号哦),这在JSON中是必须的。

stringify()和parse()

可以这么理解:JSON.stringify()是从一个object中解析成JSON数据格式,而JSON.parse()是从一个字符串中解析成JSON数据格式。

1
2
3
4
5
6
7
8
9
10
var person = {
name: 'nicholas',
age: 29
};

var jsonText = JSON.stringify(person);

console.log(jsonText);

// {"name":"nicholas","age":29}
1
2
3
4
var strPerson = '{"name":"nicholas","age":29}';
var jsonText = JSON.parse(strPerson);

console.log(jsonText); // { name: 'nicholas', age: 29 }

XMLHttpRequest对象

XMLHttpRequest对象用于在后台与服务器交换数据。它是Ajax技术的核心[p571]。

XMLHttpRequest对象能够使你:

  • 在不重新加载页面的情况下更新网页
  • 在页面已加载后从服务器请求数据
  • 在页面已加载后从服务器接收数据
  • 在后台向服务器发送数据

XMLHttpRequest的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 创建XHR对象 => open()准备发送 => send()传送数据

// 创建对象,对浏览器做兼容
function createXHR(){
if(typeof XMLHttpRequest != 'undefined'){ // IE7+和其他浏览器支持
return new XMLHttpRequest();
}else if(typeof ActiveXObject != 'undefined'){
if(typeof arguments.callee.activeXString != 'string'){
var versions = ['MSXML2.XMLHttp.6.0','MSXML2.XMLHttp.3.0','MSXML2.XMLHttp']; // 低版的ie可能遇到三种不同版本的XMR对象
var i , len;
for(i = 0,len = versions.length; i < len ; i++){
try{
new ActiveXObject(version[i]);
arguments.callee.activeXString = versions[i];
break;
}catch (ex){
// 跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
}else{
throw new Error("No XHR object available.");
}
}
var xhr = createXHR();

// 准备发送数据
xhr.open("get","path/to/example.txt",false);// 非异步,异步的话第三个参数改为true

// 传送数据
xhr.send(null); // get方法不需要传数据

// 判断状态嘛,获取服务器返回的数据
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.responseText);
}else{
console.log("Request was nsuccessful : " + xhr.status);
}

跨域解决方案

何为跨域呢?只要访问的资源的协议、域名、端口三个不全相同,就可以说是非同源策略而产生了跨域了,这是狭义的说法。广义的说法:通过XHR实现Ajax通信的一个主要限制,来源于跨域的安全策略;默认情况下,XHR对象只能访问包含它的页面位于同一个域中的资源[p582]。注:部分文字和代码引用自前端常见跨域解决方案(全)

CORS

CORS(Cross-Origin Resource Sharing,跨资源共享)定义了在必须访问跨资源时,浏览器与服务器应该如何沟通。其背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。 复杂的跨域请求应当考虑使用它。

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无需设置,如果要带cookie请求:前后端都要设置。

1.前端设置

1.) 原生ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function createCORSRequest(method,url){ // 兼容处理,ie8/9需要用到window.XDomainRequest
var xhr = new XMLHttpRequest();
// 前端设置是否带cookie
xhr.withCredentials = true;

if("withCredentials" in xhr){ // 其他的用到withCredentials
xhr.open(method,url,true);
}else if(typeof XDomainRequest != 'undefined'){
xhr = new XDomainRequest();
xhr.open(method , url);
}else{
xhr = null;
}

return xhr;
}

// get请求
var request = createCORSRequest("get","http://www.somewhere-else.com/page/");
if(request){
request.onload = function(){
// 对request.responseText 进行处理
};
request.send();
}

// post请求,带cookie
var requestXhr = createCORSRequest("post","http://www.somewhere-else.com/page/");
requestXhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
requestXhr.send("user=admin");
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};

2.)jquery ajax

上面写了一大堆原生的,看得头都有点大了,还是使用jquery ajax 比较舒服:

1
2
3
4
5
6
7
8
$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});

3.) vue框架

在vue-resource封装的ajax组建中加入以下代码:

1
Vue.http.options.credentials = true;

2.服务器设置

若后端设置成功,前端浏览器控制台上就不会出现跨域报错的信息,反之,说明没有成功。

1.) java后台

1
2
3
4
5
6
/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); // 若有端口需写全(协议+域名+端口)
response.setHeader("Access-Control-Allow-Credentials", "true");

2.) node后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
var postData = '';

// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});

// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);

// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取cookie
});

res.write(JSON.stringify(postData));
res.end();
});
});

server.listen('8080');
console.log('Server is running at port 8080...');

JSONP

JSONP是JSON with padding(填充式JSON或参数式JSON)的简写,是应用JSON的一种新方法,在后来的web服务中非常流行。简单的跨域请求用JSONP即可。

通常为了减轻web服务器的负载,我们把js,css,img等静态资源分离到另一台独立域名的服务器,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。

1.前端实现

1.)原生实现

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
var script = document.createElement('script');
script.type = 'text/javascript';

// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);

// 回调执行函数
function onBack(res){
console.log(JSON.stringify(res));
}
</script>

服务器返回如下(返回时即执行全局函数):

1
onBack({"status": true,"user":"admin"})

2.)jquery ajax

1
2
3
4
5
6
7
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: 'onBack', // 自定义回调函数名
data: {}
});

3.)vue.js

1
2
3
4
5
6
this.$http.jsonp('http://www.domain2.com:8080/login',{
params: {},
jsonp: 'onBack '
}).then((res)=>{
console.log(res);
});

2.后端nodejs代码的示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var qs = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request',function(req,res){
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;

// jsonp返回设置
res.writeHead(200,{"Content-Type":"text/javascript"});
res.write(fn + '('+JSON.stringify(params)+')');

res.end();
});

server.listen('8080');
console.log('Server is running at port 8080 ...');

⚠️ jsonp缺点:

  1. jsonp只支持get请求而不支持post请求,也就是说如果想传给后台一个json格式的数据,此时问题就来了,浏览器回报一个http状态码415错误,告诉你请求格式不正确。

  2. 存在明显的安全性问题,容易收到xss攻击

WebSocket协议跨域

WebSocket protocol 是 HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯。

原生的WebSocket API使用起来不太方便,示例中使用了socket.io,它很好的封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});

// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});

document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>

2.node socket后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});

// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});

requestAnimationFrame()帧动画

requestAnimationFrame 创建平滑的动画[p682]。在此之前都是使用setTimeout或者setInterval实现,requestAnimationFrame与它们相比:

  • 不需要时间间隔,会贴切浏览器的刷新频率
  • 在切换到另外的页面时,会停止运行

使用的示范如下:

1
<div id="num">1</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//  兼容浏览器
(function(){
var lastTime = 0;
var vendors = ['webkit','moz','ms','-o'];
for(var x = 0;x <vendors.length && !window.requestAnimationFrame; ++x){
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'cancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if(!window.requestAnimationFrame){
window.requestAnimationFrame = function(callback){
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function(){
callback;
},timeToCall);
lastTime = currTime - timeToCall;
return id;
}
}
if(!window.cancelAnimationFrame){
window.cancelAnimationFrame = function (id){
clearTimeout(id);
}
}
})();

// 简单的计数
var num = 1,
timer;
fn();
function fn(){
document.getElementById('num').innerText = ++num;
timer = requestAnimationFrame(fn);
}
document.onclick = function(){
cancelAnimationFrame(timer);
}
<-- 本文已结束  感谢您阅读 -->
客官,且步,赏一个呗 (@ ~ @)