Script标簽:向html插入js的方法
屬性 | 值 | 描述 |
---|---|---|
async | 立即下載下傳腳本(僅适用于外部腳本)。 | |
charset | 表示通過src屬性指定的代碼的字元集 | |
defer | 表示腳本可以延遲到文檔完全被解析和顯示之後再執行(僅适用于外部腳本)。 | |
language | script(已廢棄) | 表示編寫代碼使用的腳本語言。用 type 屬性代替它。 |
src | URL | 規定外部腳本檔案的 URL。 |
xml:space | preserve | 規定是否保留代碼中的空白。 |
type | text/xxx | language的替換屬性,表示編寫代碼使用的腳本語言的内容類型,也稱為MIME屬性。 |
所有<script>元素會按照在頁面出現的先後順序依次被解析,沒有 defer 或 async,浏覽器會立即加載并執行指定的腳本, 隻有解析完前面的script元素的内容後,才會解析後面的代碼。
使用<script>的兩種方式
- 頁面中嵌入script代碼, 隻需指定type屬性
<script type="text/javascript">
// 内部不能出現'</script>'字元串,如果必須出現,必須使用轉義标簽‘\’
function sayHi() {
console.log('hihihi');
alert('<\/script>');
}
</script>
包含在<script>元素内的代碼會從上而下依次解釋,在解釋器對<script>元素内的所有代碼求值完畢之前,頁面中的其餘内容都不會被浏覽器加載或顯示
- 包含外部js檔案, src屬性是必須的。
<script src="example.js"></script>
// 帶有src屬性的元素不應該在标簽之間包含額外的js代碼,即使包含,隻會下載下傳并執行外部檔案,内部代碼也會被忽略。
與嵌入式js代碼一樣, 在解析外部js檔案時,頁面的處理會暫時停止。
改變腳本行為的方法
1. defer: 立即下載下傳,延遲執行
加載後續文檔元素的過程将和 script.js 的加載并行進行(異步),但是 script.js 的執行要在所有元素解析完成之後。
會先與DOMContentLoaded事件執行。
延遲腳本總會按照指定他們的順序執行。
<script defer="defer" src="example.js"></script>
2. async: 異步腳本
加載和渲染後續文檔元素的過程将和 script.js 的加載與執行并行進行(異步)。
一定會在load事件之前執行,可能會在DOMContentLoaded事件之前或之後執行。
不能保證異步腳本按照他們在頁面中出現的順序執行。
<script async="async" src="example.js"></script>
差別:
- defer 和 async 在網絡讀取(下載下傳)是一樣的,都是異步的(相較于 HTML 解析)
- 它倆的差别在于腳本下載下傳完之後何時執行,顯然 defer 是最接近我們對于應用腳本加載和執行的要求的
- 關于 defer,它是按照加載順序執行腳本的
- async 則是一個亂序執行的主,反正對它來說腳本的加載和執行是緊緊挨着的,是以不管你聲明的順序如何,隻要它加載完了就會立刻執行
- async 對于應用腳本的用處不大,因為它完全不考慮依賴(哪怕是最低級的順序執行),不過它對于那些可以不依賴任何腳本或不被任何腳本依賴的腳本來說卻是非常合适的,最典型的例子:Google Analytics
基本類型和引用類型
基本類型:undefined、null、string、number、boolean、symbol
特點
- 基本類型的值是不可變得
// 任何方法都無法改變一個基本類型的值
let name = 'jay';
name.toUpperCase(); // 輸出 'JAY'
console.log(name); // 輸出 'jay'
- 基本類型的比較是值的比較
// 隻有在它們的值相等的時候它們才相等
let a = 1;
let b = true;
console.log(a == b); //true
// 用==比較兩個不同類型的變量時會進行一些類型轉換。
// 先會把true轉換為數字1再和數字1進行比較,結果就是true了
- 基本類型的變量是存放在棧區的(棧區指記憶體裡的棧記憶體)
引用類型:Object、Array、RegExp、Date、Function
也可以說是就是對象。對象是屬性和方法的集合,也就是說引用類型可以擁有屬性和方法,屬性又可以包含基本類型和引用類型
- 引用類型的值是可變的
// 我們可為為引用類型添加屬性和方法,也可以删除其屬性和方法
let person = { name: 'pig' };
person.age = 22;
person.sayName = () => console.log(person.name);
person.sayName(); // 'pig'
delete person.name;
- 引用類型的比較是引用的比較
let person1 = '{}';
let person2 = '{}';
console.log(person1 == person2); // 字元串值相同,true
let person1 = {};
let person2 = {};
console.log(person1 == person2); // 兩個對象的堆記憶體中的位址不同,false
- 引用類型的值是同時儲存在棧記憶體和堆記憶體中的對象
javascript和其他語言不同,其不允許直接通路記憶體中的位置,也就是說不能直接操作對象的記憶體空間。實際上,是操作對象的引用,是以引用類型的值是按引用通路的。準确地說,引用類型的存儲需要記憶體的棧區和堆區(堆區是指記憶體裡的堆記憶體)共同完成,棧區記憶體儲存變量辨別符和指向堆記憶體中該對象的指針,也可以說是該對象在堆記憶體的位址。
閉包
閉包就是指有權通路另一個函數作用域中的變量的函數。
官方解釋:閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能通路的所有局部變量。(詞法作用域)
通俗解釋:閉包的關鍵在于:外部函數調用之後其變量對象本應該被銷毀,但閉包的存在使我們仍然可以通路外部函數的變量對象。
當某個函數被掉用的時候,會建立一個執行環境及相應的作用域鍊。然後使用arguments和其他命名參數的值來初始化函數的活動對象。但在作用域鍊中,外部函數的活動對象始終處于第二位,外部函數的外部函數的活動對象處于第三位...直至作為作用域鍊終點的全局執行環境。
作用域鍊本質上是一個指向變量對象的指針清單,他隻引用但不實際包含變量對象。
無論什麼時候在函數中通路一個變量時,就會從作用域鍊中搜尋具有相同名字的變量,一般來講,當函數執行完畢,局部活動對象就會被銷毀,記憶體中僅儲存全部作用域的活動對象。但是,閉包不同。
建立閉包: 在一個函數内部建立另一個函數
function add() {
let a = 1;
let b = 3;
function closure() {
b++;
return a + b;
}
return closure;
}
// 閉包的作用域鍊包含着它自己的作用域,以及包含它的函數的作用域和全局作用域。
生命周期
通常,函數的作用域及其所有變量都會在函數執行結束後被銷毀。但是,在建立了一個閉包以後,這個函數的作用域就會一直儲存到閉包不存在為止。
當閉包中的函數closure從add中傳回後,它的作用域鍊被初始化為包含add函數的活動對象和全局變量對象。這樣closure就可以通路在add中定義的所有變量。更重要的是,add函數在執行完畢後,也不會銷毀,因為closure函數的作用域鍊仍然在引用這個活動對象。換句話說,當add傳回後,其執行環境的作用域鍊被銷毀,但它的活動對象仍然在記憶體中,直至closure被銷毀。
function add(x) {
function closure(y) {
return x + y;
}
return closure;
}
let add2 = add(2);
let add5 = add(5);
// add2 和 add5 共享相同的函數定義,但是儲存了不同的環境
// 在add2的環境中,x為5。而在add5中,x則為10
console.log(add2(3)); // 5
console.log(add5(10)); // 15
// 釋放閉包的引用
add2 = null;
add5 = null;
閉包中的this對象
var name = 'window';
var obj = {
name: 'object',
getName: () => {
return () => {
return this.name;
}
}
}
console.log(obj.getName()()); // window
obj.getName()()是在全局作用域中調用了匿名函數,this指向了window。
函數名與函數功能是分割開的,不要認為函數在哪裡,其内部的this就指向哪裡。
window才是匿名函數功能執行的環境。
使用注意點
1)由于閉包會讓包含函數中的變量都被儲存在記憶體中,記憶體消耗很大,是以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導緻記憶體洩露。解決方法是,在退出函數之前,将不使用的局部變量全部删除。
2)閉包會在父函數外部,改變父函數内部變量的值。是以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把内部變量當作它的私有屬性(private value),這時一定要小心,不要随便改變父函數内部變量的值。
使用
- 模仿塊級作用域
- 私有變量
- 子產品模式
在循環中建立閉包:一個常見錯誤
function show(i) {
console.log(i);
}
function showCallback(i) {
return () => {
show(i);
};
}
// 測試1【3,3,3】
const testFunc1 = () => {
// var i;
for (var i = 0; i < 3; i++) {
setTimeout(() => show(i), 300);
}
}
// 測試2 【0,1,2】
const testFunc2 = () => {
for (var i = 0; i < 3; i++) {
setTimeout(showCallback(i), 300);
}
}
// 測試3【0,1, 2】 閉包,立即執行函數
// 在閉包函數内部形成了局部作用域,每循環一次,形成一個自己的局部作用域
const testFunc3 = () => {
for (var i = 0; i < 3; i++) {
(() => {
setTimeout(() => show(i), 300);
})(i);
}
}
// 測試4【0,1, 2】let
const testFunc4 = () => {
for (let i = 0; i < 3; i++) {
setTimeout(() => show(i), 300);
}
}
setTimeout()函數回調屬于異步任務,會出現在宏任務隊列中,被壓到了任務隊列的最後,在這段代碼應該是for循環這個同步任務執行完成後才會輪到它
測試1錯誤原因:指派給 setTimeout 的是閉包。這些閉包是由他們的函數定義和在 testFunc1 作用域中捕獲的環境所組成的。這三個閉包在循環中被建立,但他們共享了同一個詞法作用域,在這個作用域中存在一個變量i。這是因為變量i使用var進行聲明,由于變量提升,是以具有函數作用域。當onfocus的回調執行時,i的值被決定。由于循環在事件觸發之前早已執行完畢,變量對象i(被三個閉包所共享)已經指向了i的最後一個值。
測試2正确原因: 所有的回調不再共享同一個環境, showCallback 函數為每一個回調建立一個新的詞法環境。在這些環境中,i 指向數組中對應的下标。
測試4正确原因:JS中的for循環體比較特殊,每次執行都是一個全新的獨立的塊作用域,用let聲明的變量傳入到 for循環體的作用域後,不會發生改變,不受外界的影響。
作用域
javascript中變量或函數産生作用、而不會對外産生影響的封閉空間。外部不可以通路内部變量或函數,但内部能夠通路外部。
ES5:
- 全局作用域:所有地方都可以通路
- 函數作用域:隻能在函數内部通路
ES6:
- 增加了塊級作用域(最近大括号的作用範圍),但僅限于let聲明的變量
作用域鍊
規則:
- 全局變量,函數申明都是屬于0級鍊,每個對象占一個位置
- 凡是看到函數就延伸一個鍊出來,一級級展開
- 通路首先看到目前函數,如果目前作用域鍊沒有定義,往上級鍊中檢查
- 如此往複,直到0級鍊,如果0級沒有,則彈出錯誤,這個變量沒有定義
詞法作用域
所謂詞法(代碼)作用域,就是代碼在編寫過程中展現出來的作用範圍,代碼一旦寫好了,沒有運作之前(不用執行),作用範圍就已經确定好了,這個就是所謂的詞法作用域。
詞法作用域的規則:
- 函數允許通路函數外部的資料
- 整個代碼結構中隻有函數才能限定作用域
- 作用規則首先使用變量提升規則分析
- 如果目前作用規則裡面有該名字,則不考慮外面的外面的名字
詞法作用域根據聲明變量的位置來确定該變量可被通路的位置。嵌套函數可擷取聲明于外部作用域的函數。
function init() {
var name = "Mozilla"; // name 是一個被 init 建立的局部變量
function displayName() { // displayName() 是内部函數,一個閉包
alert(name); // 使用了父函數中聲明的變量
}
displayName();
}
init();
var let 差別
- var聲明的變量,隻有函數才能為它建立新的作用域
let支援塊級作用域,花括号就能為它建立新的作用域
- 相同作用域,var可以反複聲明相同辨別符的變量,而let是不允許的;
- let聲明的變量禁止在聲明前通路
// 全局變量
var i = 0 ;
// 定義外部函數
function outer(){
// 通路全局變量
console.log(i); // 0
function inner1(){
console.log(i); // 0
}
function inner2(){
console.log(i); // undefined
var i = 1;
console.log(i); // 1
}
inner1();
inner2();
console.log(i); // 0
}
變量提升
在Javascript中,函數及變量的聲明都将被提升到函數的最頂部,提升的僅僅是變量的聲明,變量的指派并不會被提升。我們需要注意的是,函數的聲明與變量的聲明是不一樣的。函數的函數體也會被一起提升。
函數表達式和變量表達式隻是其聲明被提升,函數聲明是函數的聲明和實作都被提升。
function foo() {
console.log("global foo");
}
function bar() {
console.log("global bar");
}
//定義全局變量
var v = "global var";
function hoistMe() {
// var bar; 被提升到頂部,并未實作
// var v;
console.log(typeof foo); //function
console.log(typeof bar); //undefined
console.log(v); //undefined
// 函數裡面定義了同名的函數和變量,無論在函數的任何位置定義這些函數和和變量,它們都将被提升到函數的最頂部。
foo(); //local foo
bar(); //報錯,TypeError "bar is not a function"
//函數聲明,變量foo以及其實作被提升到hoistMe函數頂部
function foo() {
alert("local foo");
}
//函數表達式,僅變量bar被提升到函數頂部,實作沒有被提升
var bar = function() {
alert("local bar");
};
//定義局部變量
var v = "local";
}
let 變量提升
console.log(a); // Uncaught ReferenceError: a is not defined
let a = "I am a";
let b = "I am outside B";
if(true){
console.log(b); // Uncaught ReferenceError: b is not defined
let b = " I am inside B";
}
如果b沒有變量提升,執行到console.log時應該是輸出全局作用域中的b,而不是出現錯誤。
我們可以推知,這裡确實出現了變量提升,而我們不能夠通路的原因事實上是因為let的死區設計:目前作用域頂部到該變量聲明位置中間的部分,都是該let變量的死區,在死區中,禁止通路該變量。由此,我們給出結論,let聲明的變量存在變量提升, 但是由于死區我們無法在聲明前通路這個變量。
this 指向問題
this 就是一個指針,指向我們調用函數的對象。
執行上下文: 是語言規範中的一個概念,用通俗的話講,大緻等同于函數的執行“環境”。具體的有:變量作用域(和 作用域鍊條,閉包裡面來自外部作用域的變量),函數參數,以及 this 對象的值。
找出 this 的指向
this 的值并不是由函數定義放在哪個對象裡面決定,而是函數執行時由誰來喚起決定。
var name = "Jay Global";
var person = {
name: 'Jay Person',
details: {
name: 'Jay Details',
print: function() {
return this.name;
}
},
print: function() {
return this.name;
}
};
console.log(person.details.print()); // 【details對象調用的print】Jay Details
console.log(person.print()); // 【person對象調用的print】Jay Person
var name1 = person.print;
var name2 = person.details;
console.log(name1()); // 【name1前面沒有調用對象,是以是window】Jay Global
console.log(name2.print()) // 【name2對象調用的print】Jay Details
this和箭頭函數
箭頭函數按詞法作用域來綁定它的上下文,是以 this 實際上會引用到原來的上下文。
箭頭函數保持它目前執行上下文的詞法作用域不變,而普通函數則不會。換句話說,箭頭函數從包含它的詞法作用域中繼承到了 this 的值。
匿名函數,它不會作為某個對象的方法被調用, 是以,this 關鍵詞指向了全局 window 對象。
var object = {
data: [1,2,3],
dataDouble: [1,2,3],
double: function() {
console.log(this); // object
return this.data.map(function(item) { // this是目前object,object調用的double
console.log(this); // 傳給map()的那個匿名函數沒有被任一對象調用,是以是window
return item * 2;
});
},
doubleArrow: function() {
console.log(this); // object
return this.dataDouble.map(item => { // this是目前object,object調用的doubleArrow
console.log(this); // doubleArrow是object調用的,這就是上下文,是以是window
return item * 2;
});
}
};
object.double();
object.doubleArrow();
明确設定執行上下文
在 JavaScript 中通過使用内置的特性開發者就可以直接操作執行上下文了。這些特性包括:
- bind():不需要執行函數就可以将 this 的值準确設定到你選擇的一個對象上。通過逗号隔開傳遞多個參數。 設定好 this 關鍵詞後不會立刻執行函數。
- apply():将 this 的值準确設定到你選擇的一個對象上。apply(thisObj, argArray)接收兩個參數,thisObj是函數運作的作用域(this),argArray是參數數組,數組的每一項是你希望傳遞給函數的參數。如果沒有提供argArray和thisObj任何一個參數,那麼Global對象将用作thisObj。最後,會立刻執行函數。
- call():将 this 的值準确設定到你選擇的一個對象上。然後像bind 一樣通過逗号分隔傳遞多個參數給函數。文法:call(thisObj,arg1,arg2,..., argn);,如果沒有提供thisObj參數,那麼Global對象被用于thisObj。最後,會立刻執行函數。
this 和 bind
var bobObj = {
name: "Bob"
};
function print() {
return this.name;
}
var printNameBob = print.bind(bobObj);
console.log(printNameBob()); // Bob
this 和 call
function add(a, b) {
return a + b;
}
function sum() {
return Array.prototype.reduce.call(arguments, add);
}
console.log(sum(1,2,3,4)); // 10
this 和 apply
apply 就是接受數組版本的call。
Math.min(1,2,3,4); // 傳回 1
Math.min([1,2,3,4]); // 傳回 NaN。隻接受數字
Math.min.apply(null, [1,2,3,4]); // 傳回 1
function Person(name, age){
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
Person.apply(this, arguments); //Person.call(this, name, age);
this.grade = grade;
}
var student = new Student("sansan", 21, "一年級");
console.log("student:", student); // {name: 'sansan'; age: '21', grade: '一年級'}
如果你的參數本來就存在一個數組中,那自然就用 apply,如果參數比較散亂互相之間沒什麼關聯,就用 call。
對象屬性類型
資料屬性
資料屬性包含一個資料值的位置,在這個位置可以讀取和寫入值,資料屬性有4個描述其行為的特性:
- Configurable: 表示是否通過delete删除屬性進而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為通路器屬性。預設值是true
- Enumerable: 表示能否通過for-in循環傳回屬性。預設值是true
- Writable: 表述能否修改屬性。預設值是true
- Value: 包含這個屬性的資料值。預設值是true
通路器屬性
函數式程式設計
函數式程式設計是一種程式設計範式,是一種建構計算機程式結構和元素的風格,它把計算看作是對數學函數的評估,避免了狀态的變化和資料的可變。
純函數
純函數是穩定的、一緻的和可預測的。給定相同的參數,純函數總是傳回相同的結果。
純函數有一個非常嚴格的定義:
- 如果給定相同的參數,則傳回相同的結果(也稱為确定性)。
- 它不會引起任何副作用。
純函數特性
- 如果給定相同的參數,則得到相同的結果
我們想要實作一個計算圓的面積的函數。
不是純函數會這樣做:
let PI = 3.14;
const calculateArea = (radius) => radius * radius * PI;
// 它使用了一個沒有作為參數傳遞給函數的全局對象
calculateArea(10); // returns 314.0
純函數:
let PI = 3.14;
const calculateArea = (radius, pi) => radius * radius * pi;
// 現在把 PI 的值作為參數傳遞給函數,這樣就沒有外部對象引入。
calculateArea(10, PI); // returns 314.0
- 無明顯副作用
純函數不會引起任何可觀察到的副作用。可見副作用的例子包括修改全局對象或通過引用傳遞的參數。
現在,實作一個函數,接收一個整數并返對該整數進行加1操作且傳回。
let counter = 1;
function increaseCounter(value) {
counter = value + 1;
}
increaseCounter(counter);
console.log(counter); // 2
該非純函數接收該值并重新配置設定counter,使其值增加1。
函數式程式設計不鼓勵可變性(修改全局對象)。
let counter = 1;
const increaseCounter = (value) => value + 1; // 函數傳回遞增的值,而不改變變量的值
increaseCounter(counter); // 2
console.log(counter); // 1
純函數的好處
純函數代碼肯定更容易測試,不需要 mock 任何東西,是以,我們可以使用不同的上下文對純函數進行單元測試:
一個簡單的例子是接收一組數字,并對每個數進行加 1
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
incrementNumbers(list); // [2, 3, 4, 5, 6]
對于輸入[1,2,3,4,5],預期輸出是[2,3,4,5,6]。
引用透明性
實作一個square 函數:
const square = (n) => n * n;
square(2); // 4 将2作為square函數的參數傳遞始終會傳回4
可以把square(2)換成4,我們的函數就是引用透明的。
如果一個函數對于相同的輸入始終産生相同的結果,那麼它可以看作透明的。
函數也可以被看作成值并用作資料使用。
- 從常量和變量中引用它。
- 将其作為參數傳遞給其他函數。
- 作為其他函數的結果傳回它。
其思想是将函數視為值,并将函數作為資料傳遞。通過這種方式,我們可以組合不同的函數來建立具有新行為的新函數。
假如我們有一個函數,它對兩個值求和,然後将值加倍,如下所示:
const doubleSum = (a, b) => (a + b) * 2;
對應兩個值求差,然後将值加倍:
const doubleSubtraction = (a, b) => (a - b) * 2
這些函數具有相似的邏輯,但差別在于運算符的功能。如果我們可以将函數視為值并将它們作為參數傳遞,我們可以建構一個接收運算符函數并在函數内部使用它的函數。
const sum = (a, b) => a + b;
const subtraction = (a, b) => a - b;
const doubleOperator = (f, a, b) => f(a, b) * 2;
doubleOperator(sum, 3, 1); // 8
doubleOperator(subtraction, 3, 1); // 4
Promise
Promise 必須為以下三種狀态之一:等待态(Pending)、執行态(Fulfilled)和拒絕态(Rejected)。一旦Promise 被 resolve 或 reject,不能再遷移至其他任何狀态(即狀态 immutable)。
基本過程:
- 初始化 Promise 狀态(pending)
- 執行 then(..) 注冊回調處理數組(then 方法可被同一個 promise 調用多次)
- 立即執行 Promise 中傳入的 fn 函數,将Promise 内部 resolve、reject 函數作為參數傳遞給 fn ,按事件機制時機處理
- Promise中要保證,then方法傳入的參數 onFulfilled 和 onRejected,必須在then方法被調用的那一輪事件循環之後的新執行棧中執行。
真正的鍊式Promise是指在目前promise達到fulfilled狀态後,即開始進行下一個promise.
跨域
不同位址,不同端口,不同級别,不同協定都會構成跨域。
跨域方案
window.postMessage
window.postMessage是html5的功能,是用戶端和用戶端直接的資料傳遞,既可以跨域傳遞,也可以同域傳遞。
栗子:假如有一個頁面,頁面中拿到部分使用者資訊,點選進入另外一個頁面,另外的頁面預設是取不到使用者資訊的,你可以通過window.postMessage把部分使用者資訊傳到這個頁面中。(需要考慮安全性等方面。)
發送資訊
//彈出一個新視窗
var domain = 'http://haorooms.com';
var myPopup = window.open(`${domain}/windowPostMessageListener.html`,'myWindow');
//周期性的發送消息
setTimeout(function(){
var message = {name:"站點",sex:"男"}; //你在這裡也可以傳遞一些資料,obj等
console.log('傳遞的資料是 ' + message);
myPopup.postMessage(message, domain);
},1000);
要延遲一下,我們一般用計時器setTimeout延遲再發用。
接受的頁面
//監聽消息回報
window.addEventListener('message',function(event) {
if(event.origin !== 'http://haorooms.com') return;
//這個判斷一下是不是我這個域名跳轉過來的
console.log('received response: ', event.data);
}, false);
如下圖,接受頁面得到資料

如果是使用iframe,代碼應該這樣寫:
//捕獲iframe
var domain = 'http://haorooms.com';
var iframe = document.getElementById('myIFrame').contentWindow;
//發送消息
setTimeout(function(){
var message = {name:"站點",sex:"男"};
console.log('傳遞的資料是: ' + message);
iframe.postMessage(message, domain);
},1000);
接受資料
//響應事件
window.addEventListener('message',function(event) {
if(event.origin !== 'http://haorooms.com') return;
console.log('message received: ' + event.data,event);
event.source.postMessage('holla back youngin!',event.origin);
},false);
上面的代碼片段是往消息源回報資訊,确認消息已經收到。下面是幾個比較重要的事件屬性:
**source – 消息源,消息的發送視窗/iframe。
origin – 消息源的URI(可能包含協定、域名和端口),用來驗證資料源。
data – 發送方發送給接收方的資料。**
CORS跨域資源共享
CORS跨域和jsonp跨域的優勢:
CORS與JSONP相比,無疑更為先進、友善和可靠。
1、 JSONP隻能實作GET請求,而CORS支援所有類型的HTTP請求。
2、 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得資料,比起JSONP有更好的錯誤處理。
3、 JSONP主要被老的浏覽器支援,它們往往不支援CORS,而絕大多數現代浏覽器都已經支援了CORS
CORS的使用
CORS要前後端同時做配置。
1、首先我們來看前端。
純js的ajax請求。
<script type="text/javascript">
var xhr = new XMLHttpRequest();
// ie6以下用new ActiveXObject("Microsoft.XMLHTTP");可以做能力判斷。
xhr.open("GET", "/haorooms", true);
xhr.send();
</script>
以上的haorooms是相對路徑,如果我們要使用CORS,相關Ajax代碼可能如下所示:
<script type="text/javascript">
var xhr = new XMLHttpRequest();
//ie6以下用new ActiveXObject("Microsoft.XMLHTTP");可以做能力判斷。
xhr.open("GET", "http://www.haorooms.com/CORS", true);
xhr.send();
</script>
當然,你也可以用jquery的ajax進行。
2、後端或者伺服器端的配置
下面我們主要介紹Apache和PHP裡的設定方法。
Apache:Apache需要使用mod_headers子產品來激活HTTP頭的設定,它預設是激活的。你隻需要在Apache配置檔案的 < Directory >, < Location>, < Files >或< VirtualHost>的配置裡加入以下内容即可:
Header set Access-Control-Allow-Origin *
PHP:隻需要使用如下的代碼設定即可。
<?php
header("Access-Control-Allow-Origin:*");
以上的配置的含義是允許任何域發起的請求都可以擷取目前伺服器的資料。當然,這樣有很大的危險性,惡意站點可能通過XSS攻擊我們的伺服器。是以我們應該盡量有針對性的對限制安全的來源,例如下面的設定使得隻有www.haorooms.com這個域才能跨域通路伺服器的API。
Access-Control-Allow-Origin: http://www.haorooms.com
通過jsonp跨域
jsonp跨域也需要前後端配合使用。一般後端設定callback ,前端給背景接口中傳一個callback 就可以。
例如前端代碼:
<script type="text/javascript">
function dosomething(jsondata){
//處理獲得的json資料
}
</script>
<script src="http://haorooms.com/data.php?callback=dosomething"></script>
背景代碼:
<?php
$callback = $_GET['callback'];//得到回調函數名
$data = array('a','b','c');//要傳回的資料
echo $callback.'('.json_encode($data).')';//輸出
?>
假如你用ajax方式進行jsonp跨域:
$.ajax({
type : "get",
url : "跨域位址",
dataType : "jsonp",//資料類型為jsonp
jsonp: "callback",
//服務端用于接收callback調用的function名的參數【背景接受什麼參數,我們就傳什麼參數】
success : function(data){
//結果處理
},
error:function(data){
console.log(data);
}
});
通過修改document.domain來跨子域
我們隻需要在跨域的兩個頁面中設定document.domain就可以了。修改document.domain的方法隻适用于不同子域的架構間的互動。
栗子:
1.在頁面 http:// www.haorooms.com/a.html 中設定document.domain
<iframe id = "iframe" src="http://haorooms.com/b.html" onload = "test()"></iframe>
<script type="text/javascript">
document.domain = 'haorooms.com';//設定成主域
function test(){
alert(document.getElementById('iframe').contentWindow);
//contentWindow 可取得子視窗的 window 對象
}
</script>
2、在頁面http:// haorooms.com/b.html 中設定document.domain
<script type="text/javascript">
document.domain = 'haorooms.com';
//在iframe載入這個頁面也設定document.domain,使之與首頁面的document.domain相同
</script>
使用window.name來進行跨域
原理:
window對象有個name屬性,該屬性有個特征:即在一個視窗(window)的生命周期内,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個視窗載入過的所有頁面中的。
方法:
假如有三個頁面。
a.com/app.html:應用頁面。
a.com/proxy.html:代理檔案,一般是一個沒有任何内容的html檔案,需要和應用頁面在同一域下。
b.com/data.html:應用頁面需要擷取資料的頁面,可稱為資料頁面。
1、在應用頁面(a.com/app.html)中建立一個iframe,把其src指向資料頁面(b.com/data.html)。
資料頁面會把資料附加到這個iframe的window.name上,data.html代碼如下:
<script type="text/javascript">
window.name = 'I was there!';
// 這裡是要傳輸的資料,大小一般為2M,IE和firefox下可以大至32M左右
// 資料格式可以自定義,如json、字元串
</script>
2、在應用頁面(a.com/app.html)中監聽iframe的onload事件,在此事件中設定這個iframe的src指向本地域的代理檔案(代理檔案和應用頁面在同一域下,是以可以互相通信)。
app.html部分代碼如下:
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 讀取資料
alert(data);
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html";
// 設定的代理檔案
}
};
iframe.src = 'http://b.com/data.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>
3、擷取資料以後銷毀這個iframe,釋放記憶體;這也保證了安全(不被其他域frame js通路)。
<script type="text/javascript">
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
</script>
webpack解決跨域問題
webpack也可以解決前端跨域問題,隻需要安裝webpack 的http-proxy-middleware子產品就可以
npm install http-proxy-middleware --save-dev
配置如下:
module.exports = {
devtool: 'cheap-module-source-map',
entry: './app/js/index.js'
output: {
path: path.resolve(__dirname, 'dev'),
// 所有輸出檔案的目标路徑
filename: 'js/bundle.js',
publicPath: '/',
chunkFilename: '[name].chunk.js'
},
devServer: {
contentBase: path.resolve(__dirname, 'dev'),
publicPath: '/',
historyApiFallback: true,
proxy: {
// 請求到 '/device' 下的請求都會被代理到target:http://debug.haorooms.com中
'/device/*': {
target: 'http://debug.haorooms.com',
secure: false, // 接受運作在https上的服務
changeOrigin: true
}
}
}
}
使用如下:
fetch('/device/space').then(res => {
// 被代理到 http://debug.haorooms.com/device/space
return res.json();
});
fetch('device/space').then(res => {
// http://localhost:8080/device/space 通路本地服務
return res.json();
});
// 注:使用的url 必須以/開始 否則不會代理到指定位址
cookie跨域
業務場景
1、百度www域名下面登入了,發現yun域名下面也自然而然登入了。
2、淘寶登入了,發現天貓也登入了,淘寶和天貓是完全不一樣的2個域名。
栗子
- 同一個主域下面的跨域問題,類似www.baidu 和yun.baidu
cookie屬性:
path
cookie 一般都是由于使用者通路頁面而被建立的,但是并不是隻有在建立 cookie 的頁面才可以通路這個cookie。在預設情況下,出于安全方面的考慮,隻有與建立 cookie 的頁面處于同一個目錄或在建立cookie頁面的子目錄下的網頁才可以通路。那麼此時如果希望其父級或者整個網頁都能夠使用cookie,就需要進行路徑的設定。
path表示cookie所在的目錄,haorooms.com預設為/,就是根目錄。 在同一個伺服器上有目錄如下:
/post/,/post/id/,/tag/,/tag/haorooms/,/tag/id/
現假設一個
cookie1的path為/tag/,
cookie2的path為/tag/id/,
那麼tag下的所有頁面都可以通路到cookie1,而/tag/和/tag/haorooms/的子頁面不能通路cookie2。 這是因為cookie2能讓其path路徑下的頁面通路。
讓這個設定的cookie 能被其他目錄或者父級的目錄通路的方法:
document.cookie = "name = value; path=/";
domain
domain表示的是cookie所在的域,預設為請求的位址,
如網址為 http://www.haorooms.com/post/... ,那麼domain預設為www.haorooms.com。
而跨域通路,
如域A為love.haorooms.com,域B為resource.haorooms.com,
那麼在域A生産一個令域A和域B都能通路的cookie就要将該cookie的domain設定為.haorooms.com;
如果要在域A生産一個令域A不能通路而域B能通路的cookie就要将該cookie的domain設定為resource.haorooms.com。
這樣,我們就知道為什麼www.百度 和yun.baidu共享cookie,我們隻需要設定domain為.baidu.com就可以了【注:點好是必須的】
- 天貓和淘寶是如何共享cookie的?
cookie跨域解決方案一般有如下幾種:
1、nginx反向代理
反向代理(Reverse Proxy)方式是指以代理伺服器來接受Internet上的連接配接請求,然後将請求轉發給内部網絡上的伺服器;并将從伺服器上得到的結果傳回給Internet上請求連接配接的用戶端,此時代理伺服器對外就表現為一個伺服器。
反向代理伺服器對于用戶端而言它就像是原始伺服器,并且用戶端不需要進行任何特别的設定。用戶端向反向代理 的命名空間(name-space)中的内容發送普通請求,接着反向代理将判斷向何處(原始伺服器)轉交請求,并将獲得的内容傳回給用戶端,就像這些内容 原本就是它自己的一樣。
nginx配置如下:
upstream web1{
server 127.0.0.1:8089 max_fails=0 weight=1;
}
upstream web2 {
server 127.0.0.1:8080 max_fails=0 weight=1;
}
location /web1 {
proxy_pass http://web1;
proxy_set_header Host 127.0.0.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
log_subrequest on;
}
location /web2 {
proxy_pass http://web2;
proxy_set_header Host 127.0.0.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie $http_cookie;
log_subrequest on;
}
3、nodejs superagent
package.json中的子產品依賴:
調用superagent api請求:
其實本質也是jsonp的方式。
同一域下,不同工程下的cookie攜帶問題
cookie跨域通路之後,可以成功的寫入本地域。本地的前端工程在請求後端工程時,有很多是ajax請求,ajax預設不支援攜帶cookie,是以現在有以下兩種方案:
(1). 使用jsonp格式發送
(2). ajax請求中加上字段 xhrFields: {withCredentials: true},這樣可以攜帶上cookie
伺服器需要配置:
Access-Control-Allow-Credentials: true
localStorage跨域
子產品化
AMD、CMD 和 CommonJS 的子產品化開發
AMD/CMD/CommonJs都是JS子產品化開發的标準,目前對應的實作是RequireJS,SeaJs, nodeJs;
CommonJS:服務端js
CommonJS 是以在浏覽器環境之外建構 javaScript 生态系統為目标而産生的寫一套規範,主要是為了解決 javaScript 的作用域問題而定義的子產品形式,可以使每個子產品它自身的命名空間中執行。
實作方法:子產品必須通過 module.exports 導出對外的變量或者接口,通過 require() 來導入其他子產品的輸出到目前子產品的作用域中;
主要針對服務端(同步加載檔案)和桌面環境中,node.js 遵循的是 CommonJS 的規範;
CommonJS 加載子產品是同步的,是以隻有加載完成才能執行後面的操作.
**require()用來引入外部子產品;
exports對象用于導出目前子產品的方法或變量,唯一的導出口;
module對象就代表子產品本身。**
// 定義一個module.js檔案
var A = () => console.log('我是定義的子產品');
// 1.第一種傳回方式
module.exports = A;
// 2.第二種傳回方式
module.exports.test = A
// 3.第三種傳回方式
exports.test = A;
// 定義一個test.js檔案【這兩個檔案在同一個目錄下】
var module = require("./module");
//調用這個子產品,不同的傳回方式用不同的方式調用
// 1.第一種調用方式
module();
// 2.第二種調用方式
module.test();
// 3.第三種調用方式
module.test();
// 執行檔案
node test.js
AMD: 異步子產品定義【浏覽器端js】
AMD 是 Asynchronous Module Definition 的縮寫,意思是異步子產品定義;采用的是異步的方式進行子產品的加載,在加載子產品的時候不影響後邊語句的運作。主要是為前端 js 的表現指定的一套規範。
實作方法:通過define方法去定義子產品,通過require方法去加載子產品。
define(id?,dependencies?,factory): 它要在聲明子產品的時候制定所有的依賴(dep),并且還要當做形參傳到factory中。沒什麼依賴,就定義簡單的子產品(或者叫獨立的子產品)
require([modules], callback):第一個參數[modules],是需加載的子產品名數組;第二個參數callback,是子產品加載成功之後的回調函數
主要針對浏覽器js,requireJs遵循的是 AMD 的規範;
// module1.js檔案, 定義獨立的子產品
define({
methodA: () => console.log('我是module1的methodA');
methodB: () => console.log('我是module1的methodB');
});
// module2.js檔案, 另一種定義獨立子產品的方式
define(() => {
return {
methodA: () => console.log('我是module2的methodA');
methodB: () => console.log('我是module2的methodB');
};
});
// module3.js檔案, 定義非獨立的子產品(這個子產品依賴其他子產品)
define(['module1', 'module2'], (m1, m2) => {
return {
methodC: () => {
m1.methodA();
m2.methodB();
}
};
});
//定義一個main.js,去加載這些個子產品
require(['module3'], (m3) => {
m3.methodC();
});
// 為避免造成網頁失去響應,解決辦法有兩個,一個是把它放在網頁底部加載,另一個是寫成下面這樣:
<script src="js/require.js" defer async="true" ></script>
// async屬性表明這個檔案需要異步加載,避免網頁失去響應。
// IE不支援這個屬性,隻支援defer,是以把defer也寫上。
// data-main屬性: 指定網頁程式的主子產品
<script data-main="main" src="js/require.js"></script>
// 控制台輸出結果
我是module1的methodA
我是module2的methodB
CMD: 通用子產品定義【浏覽器端js】
CMD 是 Common Module Definition 的縮寫,通過異步的方式進行子產品的加載的,在加載的時候會把子產品變為字元串解析一遍才知道依賴了哪個子產品;
主要針對浏覽器端(異步加載檔案),按需加載檔案。對應的實作是seajs
AMD和CMD的差別:
1. 對于依賴的子產品,AMD 是提前執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改成可以延遲執行(根據寫法不同,處理方式不同)。CMD 推崇 as lazy as possible(盡可能的懶加載,也稱為延遲加載,即在需要的時候才加載)。
2. CMD 推崇依賴就近,AMD 推崇依賴前置。
// CMD
define(function(require, exports, module) {
var a = require('./a');
a.doSomething();
// ...
var b = require('./b'); // 依賴可以就近書寫
b.doSomething();
// ...
})
// AMD
define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好
a.doSomething();
// ...
b.doSomething();
//...
})
import和require
import和require都是被子產品化使用
- require是CommonJs的文法(AMD規範引入方式),CommonJs的子產品是對象。
- import是es6的一個文法标準(浏覽器不支援,本質是使用node中的babel将es6轉碼為es5再執行,import會被轉碼為require),es6子產品不是對象
- require是運作時加載整個子產品(即子產品中所有方法),生成一個對象,再從對象上讀取它的方法(隻有運作時才能得到這個對象,不能在編譯時做到靜态化),理論上可以用在代碼的任何地方。
- import是編譯時調用,确定子產品的依賴關系,輸入變量(es6子產品不是對象,而是通過export指令指定輸出代碼,再通過import輸入,隻加載import中導的方法,其他方法不加載),import具有提升效果,會提升到子產品的頭部(編譯時執行)
export和import可以位于子產品中的任何位置,但是必須是在子產品頂層,如果在其他作用域内,會報錯
es6這樣的設計可以提高編譯器效率,但沒法實作運作時加載
- require是指派過程,把require的結果(對象,數字,函數等),預設是export的一個對象,賦給某個變量(複制或淺拷貝)
- import是解構過程(需要誰,加載誰)
// require/exports(僅有下面的三種簡單寫法)
const a = require('a') //真正被require出來的是來自module.exports指向的記憶體塊内容
exports.a = a //exports 隻是 module.exports的引用,輔助module.exports操作記憶體中的資料
module.exports = a
// import/export
import a from 'a'
import { default as a } from 'a'
import * as a from 'a'
import { fun1,fun2 } from 'a'
export default a
export const a=1
export functon a{ }
export { fun1,fun2 }
Require.js
require.js的誕生,就是為了解決這兩個問題:
(1)實作js檔案的異步加載,避免網頁失去響應;
(2)管理子產品之間的依賴性,便于代碼的編寫和維護。
加載js檔案
加載require.js,也可能造成網頁失去響應。解決辦法有兩個,一個是把它放在網頁底部加載,另一個是寫成下面這樣:
<script src="js/require.js" defer async="true" ></script>
async屬性表明這個檔案需要異步加載,避免網頁失去響應。
IE不支援async,隻支援defer,是以把defer也寫上。
require.config()
在用require()加載之前,要先用require.config()方法,定義它們的一些特征
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
},
shim: {
'underscore':{
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
});
從 URL 輸入到頁面展現大緻流程概述:
- 在浏覽器輸入 URL;
- 浏覽器查找域名對應的 IP 位址;
- 浏覽器根據 IP 位址與伺服器建立聯系;
- 浏覽器與伺服器通信:浏覽器請求,伺服器處理請求并呈現頁面。
具體流程概述
第一步,在浏覽器輸入 URL
在位址欄輸入相應的 URL 。
第二步,浏覽器查找域名對應的 IP 位址
第一步中,我們已經輸入了相應的 URL,但浏覽器本身并不能識别 URL 是什麼,是以從我們輸入 URL 開始,浏覽器就要進行域名解析來找到對應 IP——DNS 解析是浏覽器的實際尋址方式:
- 查找浏覽器緩存——近期浏覽過的網站,浏覽器會緩存 DNS 記錄一段時間 (如果沒有則往下) ;
- 查找系統緩存——從 C 盤的 hosts 檔案查找是否有存儲的 DNS 資訊,查找是否有目标域名和對應的 IP 位址 (如果沒有則往下);
- 查找路由器緩存 (如果沒有則往下);
- 查找 ISP DNS 緩存——從網絡服務商(比如電信)的 DNS 緩存資訊中查找(如果沒有則往下);
- 經由以上方式都沒找到對應 IP 的話,就會向根域名伺服器查找目标 URL 對應的 IP,根域名伺服器會向下級伺服器轉達請求,層層下發,直到找到對應的 IP 為止。
第三步,浏覽器根據 IP 位址與伺服器建立聯系
根據IP建立TCP連接配接(三次握手)
第 2 步中,浏覽器通過 IP 尋址找到了對應的伺服器,浏覽器就将使用者發起的 HTTP 請求發送給伺服器。
伺服器開始處理使用者請求:
- 每台伺服器上都會安裝處理請求的應用——Web Server;
- 常見的 Web Server 産品有:Apache 、Nginx 、IIS 和 lighttpd 等;
- Web Server 可以了解為一個管理者,它不做具體的請求處理,而是會結合配置檔案,把不同使用者發來的請求委托給伺服器上專門處理相應請求的程式(伺服器上的相應程式開始處理請求的這一部分,通俗說就是實際背景處理的工作):
背景開發現在有很多架構,但大部分都是按照 MVC(Model View Controller)設計模式搭建的,它将伺服器上的應用程式分成 3 個核心部件且分别處理自己的任務,實作輸入、處理、輸出的分離:
- 模型(Model)
模型,是将實際開發過程中的業務規則和所涉及的資料格式進行模型化;
應用于模型的代碼隻需寫一次就可以被多個視圖重用;
在 MVC 三個部件中,模型擁有最多的處理任務;
一個模型能為多個視圖提供資料。
- 視圖(View)
視圖是使用者看到并與之互動的界面。
- 控制器(Controller)
作用:接受使用者的輸入并調用模型(M)和視圖(V)去完成使用者的需求;
地位:控制器也是處于一個管理者的地位——從視圖(V)接收請求并決定調用哪一個模型構件(M)來處理請求,然後再确定用哪個視圖(V)來顯示模型(M)處理傳回的資料。
總而言之:
首先,控制器(C)接收使用者的請求,并決定應該調用哪個模型(M)來進行處理;
然後,模型(M)用業務邏輯來處理使用者的請求并傳回資料;
最後,控制器(C)再用相應的視圖(V)來格式化模型(M),進而傳回 HTML 字元串給浏覽器。
第四步,浏覽器與伺服器通信
在上邊的第 3 步中,伺服器傳回了 HTML 字元串給浏覽器,此時,浏覽器将會對其進行解析、渲染并最終繪制網頁:
- 加載
- 浏覽器對一個 HTML 頁面的加載順序是從上而下的;
- 浏覽器在加載的過程中,同時進行解析、渲染處理;
- 在這個過程中,遇到 link 标簽、image 标簽、script 标簽時,浏覽器會再次向伺服器發送請求以擷取相應的 CSS 檔案、圖檔資源、JS 檔案,并執行 JS 代碼,同步進行加載、解析。
- 解析、渲染
- 解析的過程,其實就是生成“樹”(Document Object Model 文檔對象模型);
- DOM 樹是由 DOM 元素及屬性節點組成,并且加上 CSS 解析的樣式對象和 JS 解析後的動作實作;
- 渲染:就是将 DOM 樹進行可視化表示。
- 繪制網頁
- 浏覽器通過渲染,将 DOM 樹可視化,得到渲染樹;
- 建構渲染樹使頁面以正确的順序繪制出來,浏覽器遵循一定的渲染規則,實作網站頁面的繪制,并最終完成頁面的展示。
第五步:關閉TCP連接配接(四次揮手)
http和https的差別
Http:超文本傳輸協定(Http,HyperText Transfer Protocol)是網際網路上應用最為廣泛的一種網絡協定。設計Http最初的目的是為了提供一種釋出和接收HTML頁面的方法。它可以使浏覽器更加高效。Http協定是以明文方式發送資訊的,如果黑客截取了Web浏覽器和伺服器之間的傳輸封包,就可以直接獲得其中的資訊。
Https:是以安全為目标的Http通道,是Http的安全版。Https的安全基礎是SSL。SSL協定位于TCP/IP協定與各種應用層協定之間,為資料通訊提供安全支援。SSL協定可分為兩層:SSL記錄協定(SSL Record Protocol),它建立在可靠的傳輸協定(如TCP)之上,為高層協定提供資料封裝、壓縮、加密等基本功能的支援。SSL握手協定(SSL Handshake Protocol),它建立在SSL記錄協定之上,用于在實際的資料傳輸開始前,通訊雙方進行身份認證、協商加密算法、交換加密密鑰等。
HTTP與HTTPS的差別
1、HTTP是超文本傳輸協定,資訊是明文傳輸,HTTPS是具有安全性的SSL加密傳輸協定。
2、HTTPS協定需要ca申請證書,一般免費證書少,因而需要一定費用。
3、HTTP和HTTPS使用的是完全不同的連接配接方式,用的端口也不一樣。前者是80,後者是443。
4、HTTP連接配接是無狀态的,HTTPS協定是由SSL+HTTP協定建構的可進行加密傳輸、身份認證的網絡協定,安全性高于HTTP協定。
https的優點
盡管HTTPS并非絕對安全,掌握根證書的機構、掌握加密算法的組織同樣可以進行中間人形式的攻擊,但HTTPS仍是現行架構下最安全的解決方案,主要有以下幾個好處:
1)使用HTTPS協定可認證使用者和伺服器,確定資料發送到正确的客戶機和伺服器;
2)HTTPS協定是由SSL+HTTP協定建構的可進行加密傳輸、身份認證的網絡協定,要比http協定安全,可防止資料在傳輸過程中不被竊取、改變,確定資料的完整性。
3)HTTPS是現行架構下最安全的解決方案,雖然不是絕對安全,但它大幅增加了中間人攻擊的成本。
4)谷歌曾在2014年8月份調整搜尋引擎算法,并稱“比起同等HTTP網站,采用HTTPS加密的網站在搜尋結果中的排名将會更高”。
Https的缺點
1)Https協定握手階段比較費時,會使頁面的加載時間延長近。
2)Https連接配接緩存不如Http高效,會增加資料開銷,甚至已有的安全措施也會是以而受到影響;
3)SSL證書通常需要綁定IP,不能在同一IP上綁定多個域名,IPv4資源不可能支撐這個消耗。
4)Https協定的加密範圍也比較有限。最關鍵的,SSL證書的信用鍊體系并不安全,特别是在某些國家可以控制CA根證書的情況下,中間人攻擊一樣可行。
for 循環
for
在for循環中,循環取得數組或是數組類似對象的值,譬如arguments和HTMLCollection對象。
不足:
- 在于每次循環的時候數組的長度都要去擷取;
- 終止條件要明确;
foreach() map()
兩個方法都可以周遊到數組的每個元素,而且參數一緻;
不同點:
forEach() : 對數組的每個元素執行一次提供的函數, 總是傳回undefined;
map() : 建立一個新數組,其結果是該數組中的每個元素都調用一個提供的函數後傳回的結果。 傳回值是一個新的數組;
var array1 = [1,2,3,4,5];
var x = array1.forEach((value,index) => {
console.log(value);
return value + 10;
});
console.log(x); // undefined
var y = array1.map((value,index) => {
console.log(value);
return value + 10;
});
console.log(y); // [11, 12, 13, 14, 15]
for in
經常用來疊代對象的屬性或數組的每個元素,它包含目前屬性的名稱或目前數組元素的索引。
當周遊一個對象的時候,變量 i 是循環計數器 為 對象的屬性名, 以任意順序周遊一個對象的可枚舉屬性。對于每個不同的屬性,語句都會被執行。
當周遊一個數組的時候,變量 i 是循環計數器 為 目前數組元素的索引
for..in循環會把某個類型的原型(prototype)中方法與屬性給周遊出來.
const array = ["admin","manager","db"];
array.color = 'red';
array.prototype.name= "zhangshan";
for(var i in array){
if(array.hasOwnProperty(i)){
console.log(array[i]); // admin,manager,db,color
}
}
// hasOwnProperty(): 對象的屬性或方法是非繼承的,傳回true
for … of
疊代循環可疊代對象(包括Array,Map,Set,String,TypedArray,arguments 對象)等等。不能周遊對象。隻循環集合本身的元素
var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
a.name = 'array';
for (var x of a) {
console.log(x); //'A', 'B', 'C'
}
for (var x of s) {
console.log(x);//'A', 'B', 'C'
}
for (var x of m) {
console.log(x[0] + '=' + x[1]);//1='x',2='y',3='z'
}
繼承
// 定義一個動物類
function Animal(name) {
// 屬性
this.name = name || 'Animal';
// 執行個體方法
this.sleep = function(){
console.log(this.name + '正在睡覺!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
原型鍊繼承
核心: 将父類的執行個體作為子類的原型
function Dog(age) {
this.age = age;
}
Dog.protoType = New Animal();
Dog.prototype.name = 'dog';
const dog = new Dog(12);
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //true
console.log(dog instanceof Dog); //true
new 建立新執行個體對象經過了以下幾步:
- 建立一個新對象
- 将新對象的_proto_指向構造函數的prototype對象
- 将構造函數的作用域指派給新對象 (也就是this指向新對象)
- 執行構造函數中的代碼(為這個新對象添加屬性)
- 傳回新的對象
// 1. 建立一個新對象
var Obj = {};
// 2. 将新對象的_proto_指向構造函數的prototype對象
Obj._proto_ = Animal.prototype();
// 3. 執行構造函數中的代碼(為這個新對象添加屬性)
Animal.call(Obj);
// 4. 傳回新的對象
return Obj;
重點:讓新執行個體的原型等于父類的執行個體。
特點:
- 執行個體可繼承的屬性有:執行個體的構造函數的屬性,父類構造函數屬性,父類原型的屬性
- 非常純粹的繼承關系,執行個體是子類的執行個體,也是父類的執行個體
-
父類新增原型方法/原型屬性,子類都能通路到
缺點:
- 新執行個體無法向父類構造函數傳參。
- 繼承單一。
- 所有新執行個體都會共享父類執行個體的屬性。(原型上的屬性是共享的,一個執行個體修改了原型屬性,另一個執行個體的原型屬性也會被修改!)
- 要想為子類新增原型上的屬性和方法,必須要在new Animal()這樣的語句之後執行,不能放到構造器中
構造函數繼承
核心:使用父類的構造函數來增強子類執行個體,等于是複制父類的執行個體屬性給子類(沒用到原型)
function Dog(name) {
Animal.apply(this, 'dog');
this.name = name;
}
const dog = new Dog();
console.log(dog.name);
console.log(dog.eat('age'));
console.log(dog instanceof Animal); //false
console.log(dog instanceof Dog); //true
重點:用.call()和.apply()将父類構造函數引入子類函數(在子類函數中做了父類函數的自執行(複制))
1、隻繼承了父類構造函數的屬性,沒有繼承父類原型的屬性。
2、解決了原型鍊繼承缺點1、2、3。
3、可以實作多繼承,繼承多個構造函數屬性(call多個)。
4、在子執行個體中可向父執行個體傳參。
- 能繼承父類構造函數的屬性。
- 無法實作構造函數的複用。(每次用每次都要重新調用)
- 每個新執行個體都有父類構造函數的副本,臃腫。
- 執行個體并不是父類的執行個體,隻是子類的執行個體
組合繼承(原型鍊繼承和構造函數繼承)(常用)
核心:通過調用父類構造,繼承父類的屬性并保留傳參的優點,然後通過将父類執行個體作為子類原型,實作函數複用
function Cat(name){
Animal.call(this, name);
this.name = name;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
重點:結合了兩種模式的優點,傳參和複用
- 可以繼承父類原型上的屬性,可以傳參,可複用。
- 每個新執行個體引入的構造函數屬性是私有的。
- 既是子類的執行個體,也是父類的執行個體
調用了兩次父類構造函數(耗記憶體),子類的構造函數會代替原型上的那個父類構造函數。
原型式繼承
重點:用一個函數包裝一個對象,然後傳回這個函數的調用,這個函數就變成了個可以随意增添屬性的執行個體或對象。object.create()就是這個原理。
特點:類似于複制一個對象,用函數來包裝。
1、所有執行個體都會繼承原型上的屬性。
2、無法實作複用。(新執行個體屬性都是後面添加的)
寄生式繼承
重點:就是給原型式繼承外面套了個殼子。
優點:沒有建立自定義類型,因為隻是套了個殼子傳回對象(這個),這個函數順理成章就成了建立的新對象。
寄生組合式繼承(常用)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 建立一個沒有執行個體方法的類
var Super = function(){};
Super.prototype = Animal.prototype;
//将執行個體作為子類的原型
Cat.prototype = new Super();
})();
var cat = new Cat();
Cat.prototype.constructor = Cat; // 需要修複下構造函數
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true