重讀《JavaScript高階程式設計》

call_me_R發表於2018-03-03

life/learn/read/javascript/javascript_high_level

最近自己在休假,打算閉門幾天將《JavaScript高階程式設計》(第3版)這本良心教材再回顧一遍。目前自己進入前端領域近兩年,現在重讀並記錄下這本教材的“硬”知識點 ? 。

函式沒有過載

ECMAScript 函式不能像傳統意義上那樣實現過載。而在其他語言(如Java)中,可以為一個函式編寫兩個定義,只要這兩個定義的簽名(接受的引數型別和數量)不同即可[p66]。ECMAScript的型別是鬆散形的,沒有簽名,所以是沒有過載的。

function load(num){
	return num + 100;
}
function load(num,name){
	return num + 200;
}
var result = load(100); // 300
# 後面的函式宣告覆蓋掉前面的函式宣告
複製程式碼

基本的資料型別

基本型別值指的是簡單的資料段,而引用型別指那些可能由多個值構成的物件[p68]。這裡指出來的基本的資料型別是說的es5的哈:Undefined,Null,Boolean,NumberString

傳遞引數

ECMAScript 中所有的函式的引數都是按值傳遞的[p70]。也就是說,把函式外部的值複製給函式內部的引數,就是把值從一個變數複製到另一個變數一樣。**基本型別值的傳遞如同基本型別變數的複製一樣,而引用型別值的傳遞,則如同引用型別變數的複製一樣。**下面分開例子介紹兩種不同型別為什麼是按值傳遞。

基本型別值

基本型別這個按值傳遞比較好理解,直接複製變數的值傳遞:

function addTen(num){
	num += 10;
	return num;
}
var count = 20;
var result = addTen(count);
console.log(result); // 30
console.log(count); // 20 ,沒有變化哈
複製程式碼

引用型別值

有些人認為引用型別的傳參是按照引用來傳的,那暫且認為他們的理解是正確的,那下面的示例結果怎麼解析呢?

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]。解析器會率先讀取函式宣告,並使其執行任何程式碼之前可用(可以訪問);至於函式表示式,則必須等到解析器執行到它所在的程式碼行,才會真正被解析。

console.log(sum(10 , 10)); // 20
function sum(num1 , num2){
	return num1 + num2;
}
複製程式碼
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物件(類陣列物件)。

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()方法時,傳遞給函式的引數必須逐個列舉出來。

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]

工廠模式

工廠模式就是造一個模子產生一個個物件。

 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型別:

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共有的方法,例項化的時候都建立了,這未免多餘了。當然可以將共有的方法提取到外面,像這樣:

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(原型)屬性,這個屬性就是一個指標,指向一個物件,而這個物件的用途就是包含可以由特定型別的所有例項共享的屬性和方法。

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

原型模式解決了函式共享的問題,但是也帶了一個問題:例項化中物件的屬性是獨立的,而原型模式這裡共享了。

組合使用建構函式模式和原型模式

建立自定義型別的最常見的方式,就是組合使用建構函式模式和原型模式。建構函式模式用於定義例項屬性,而原型模式用於定義方法和共享屬性。

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,建立物件的類中是包含了自身的屬性、方法和共有的屬性、方法,如下小狗的例子:

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);
	}
}
複製程式碼

為了看起來是類那麼一會事,動態原型模式把所有資訊都封裝在了建構函式中,而通過在建構函式中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。如下:

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();
複製程式碼

寄生建構函式模式

在前面幾種模式都不適應的情況下,可以用寄生建構函式模式(資料結構中就使用到哈),寄生建構函式模式可以看成是工廠模式和建構函式模式的結合體。其基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件。

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操作符呼叫建構函式。

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]

原型鏈

原型鏈的基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。回顧下建構函式、原型和例項的關係: 每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。

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()方法在新建立的物件上執行建構函式。

function SuperType(){
	this.colors = ['red','blue','green'];
}
function SubType(){
	// 繼承SuperType
	SuperType.call(this); // SuperType.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'
複製程式碼

上面的例子中,我在父型別建構函式中沒有傳引數,看者感興趣的話可以自己加下引數來實驗一番咯。

借用建構函式解決了原型鏈繼承的確定,但是又沒有接納原型鏈的優點:共享。下面的組合繼承結合了原型鏈和借用建構函式,容納了兩者的優點。

組合繼承

組合繼承的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對例項屬性的繼承

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()也能夠用於識別基於組合繼承建立的物件。

原型式繼承

原型式繼承是藉助原型可以基於已有的物件建立新物件,同時還不必因此建立自定義的型別。

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'
複製程式碼

寄生式繼承

寄生式繼承是與原型繼承緊密相關的一種思路。寄生式繼承的思路與寄生建構函式和工廠模式類似,即是建立了一個僅用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件,最後再像真的做了所有工作一樣返回物件。

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最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什麼情況下,都會呼叫兩次超型別建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式內部。寄生組合式繼承能夠解決這個問題。

所謂寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。其背後的基本思路是不必為了指定子型別的原型而呼叫超型別的建構函式,我們所需要的無非就是超型別的原型的一個副本而已。寄生組合式繼承的基本模式如下:

function inheritPrototype(subType,superType){
	var prototype = Object(superType.prototype); // 建立物件
	prototype.constructor = subType; // 增強物件,防止下面重寫constructor屬性
	subType.prototype = prototype; // 指定物件
	
}
複製程式碼

一個完整的例子如下,相關插圖見書[p173]:

function inheritPrototype(subType,superType){
	var prototype = Object(superType.prototype);
	prototype.constructor = subType;
	subType.prototype = prototype;
	
}
function SuperType(name){
	this.name = name;
	this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
	alert(this.name);
}
function SubType(name, age){
	SuperType.call(this, name); // 只在這呼叫了一次超型別的建構函式
        this.age = age;
}

inheritPrototype(SubType , SuperType);

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

var instance = new SubType('nicholas' , 29);
複製程式碼

上面的例子的高效處體現在它只呼叫了一次SuperType建構函式,並且避免了在SubType.prototype上建立不必要的,多餘的屬性。與此同時,原型鏈還能保持不變;因此還能正常使用instanceof和inPrototypeOf()。開發人員普遍認為寄生組合式繼承是引用型別最理想的繼承正規化。

閉包

閉包是指有權訪問另一個函式作用域中的變數的函式。我的理解是,函式內的函式使用到外層函式的變數延長變數的生存時間,造成常駐記憶體。例子見下:

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變數的,但是內部的返回的匿名函式使用到該變數,不能銷燬。如果需要銷燬的話,可以改寫成下面:

function foo(){
	var a = 2;
	return function(){
		a += 1;
		console.log(a);
	}
}
var baz = foo();
baz(); // 3

baz = null; // 將內部的匿名函式賦值為空
複製程式碼

從閉包說起

談到了閉包,這讓我想起了不久前刷知乎看到一篇文章。自己整理如下:

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。改造的方式一:

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中基本型別的引數傳遞是按值傳遞的特徵,改造程式碼如下

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塊級作用域語法,實現超級簡單:

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的情況下:

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方案:

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,我的改造如下:

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那段進行改造後如下:

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特性啦。

// 模擬其他語言中的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屬性來表示。為了相容各個瀏覽器,可以入下面這樣寫:

var leftPos = (typeof window.screenLeft == "number")?window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number")? window.screenTop : window.screenY;
複製程式碼

瀏覽器視窗大小

由於瀏覽器廠商以及歷史的問題,無法確認瀏覽器本身的大小,但是可以取得視口的大小[p198]。如下:

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;
	}
}
複製程式碼

上面的示例可以簡寫成下面這樣:

var pageWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientHeight;
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寫法:

var person = {
	name : "nicholas",
	age : 29
};

# 上面的程式碼也可以寫成下面的
var person = {
	"name" : "nicholas",
	"age" : 29
};
複製程式碼

而上面的物件寫成資料的話,就是下面這樣了:

{
	"name": "nicholas ",
	"age": 29
}

# 可到網站 https://www.bejson.com/ 驗證
複製程式碼

⚠️ 與JavaScript物件字面量相比,JSON物件又兩個地方不一樣。首先,沒有宣告變數(JSON中沒有變數的概念)。其次,沒有分號(因為這不是JavaScript語句,所以不需要分號)。留意的是,物件的屬性必須加雙引號(不是單引號哦),這在JSON中是必須的。

stringify()和parse()

可以這麼理解:JSON.stringify()是從一個object中解析成JSON資料格式,而JSON.parse()是從一個字串中解析成JSON資料格式。

var person = {
	name: 'nicholas',
	age: 29
};

var jsonText = JSON.stringify(person);

console.log(jsonText);

// {"name":"nicholas","age":29}
複製程式碼
var strPerson = '{"name":"nicholas","age":29}';
var jsonText = JSON.parse(strPerson);

console.log(jsonText); // { name: 'nicholas', age: 29 }
複製程式碼

XMLHttpRequest物件

XMLHttpRequest物件用於在後臺與伺服器交換資料。它是Ajax技術的核心[p571]。

XMLHttpRequest物件能夠使你:

  • 在不重新載入頁面的情況下更新網頁
  • 在頁面已載入後從伺服器請求資料
  • 在頁面已載入後從伺服器接收資料
  • 在後臺向伺服器傳送資料

XMLHttpRequest的使用:

# 建立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

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 比較舒服:

$.ajax({
	...
	xhrFields: {
		withCredentials: true // 前端設定是否帶cookie
	},
	crossDomain: true, // 會讓請求頭中包含跨域的額外資訊,但不會含cookie
	...
});
複製程式碼

3.) vue框架

在vue-resource封裝的ajax組建中加入以下程式碼:

Vue.http.options.credentials = true;
複製程式碼

2.伺服器設定

若後端設定成功,前端瀏覽器控制檯上就不會出現跨域報錯的資訊,反之,說明沒有成功。

1.) java後臺

/*
 * 匯入包: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後臺

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.)原生實現

<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>
複製程式碼

伺服器返回如下(返回時即執行全域性函式):

onBack({"status": true,"user":"admin"})
複製程式碼

2.)jquery ajax

$.ajax({
	url: 'http://www.domain2.com:8080/login',
	type: 'get',
	dataType: 'jsonp', // 請求方式為jsonp 
	jsonpCallback: 'onBack', // 自定義回撥函式名
	data: {}
});
複製程式碼

3.)vue.js

this.$http.jsonp('http://www.domain2.com:8080/login',{
	params: {},
	jsonp: 'onBack '
}).then((res)=>{
	console.log(res);
});
複製程式碼

2.後端nodejs程式碼的示範:

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缺點:只能實現get一種請求。

WebSocket協議跨域

WebSocket protocol 是 HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,同時允許跨域通訊。

原生的WebSocket API使用起來不太方便,示例中使用了socket.io,它很好的封裝了webSocket介面,提供了更簡單、靈活的介面,也對不支援webSocket的瀏覽器提供了向下相容。

1.前端程式碼

<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後臺

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與它們相比:

  • 不需要時間間隔,會貼切瀏覽器的重新整理頻率
  • 在切換到另外的頁面時,會停止執行

使用的示範如下:

<div id="num">1</div>
複製程式碼
//  相容瀏覽器
(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);
}
複製程式碼

原文連結請戳這裡

相關文章