- 做者:陳大魚頭
- github: KRISACHAN
在資源不足的設備上,將服務合併到瀏覽器進程中javascript
負責瀏覽器界面顯示html
各個頁面的管理,建立以及銷燬前端
將渲染進程的結果繪製到用戶界面上html5
網絡資源管理java
頁面渲染node
腳本執行android
事件處理nginx
輸入網址git
瀏覽瀏覽器解析 URLgithub
生成 HTTP 請求信息
收到響應
狀態碼 | 含義 |
---|---|
1xx | 告知請求的處理進度和狀況 |
2xx | 成功 |
3xx | 表示須要進一步操做 |
4xx | 客戶端錯誤 |
5xx | 服務端錯誤 |
Socket 庫提供查詢 IP 地址的功能
經過解析器向 DNS 服務器發出查詢
尋找相應的 DNS 服務器並獲取 IP 地址
經過緩存加快 DNS 服務器的響應
協議棧經過 TCP 協議收發數據的操做。
瀏覽器調用 Socket.connect
瀏覽器調用 Socket.write
將 HTTP 請求消息交給協議棧
對較大的數據進行拆分,拆分的每一塊數據加上 TCP 頭,由 IP 模塊來發送
使用 ACK 號確認網絡包已收到
根據網絡包平均往返時間調整 ACK 號等待時間
使用窗口有效管理 ACK 號
ACK 與窗口的合併
接收 HTTP 響應消息
瀏覽器調用 Socket.close
數據發送完畢後斷開鏈接
刪除套接字
同源策略是一個重要的安全策略,它用於限制一個origin的文檔或者它加載的腳本如何能與另外一個源的資源進行交互。它能幫助阻隔惡意文檔,減小可能被攻擊的媒介。
若是兩個 URL 的 protocol 、 port (若是有指定的話)和 host 都相同的話,則這兩個 URL 是同源。
例如:
URL | 結果 | 緣由 |
---|---|---|
http://store.company.com/dir2/other.html |
同源 | 只有路徑不一樣 |
http://store.company.com/dir/inner/another.html |
同源 | 只有路徑不一樣 |
https://store.company.com/secure.html |
失敗 | 協議不一樣 |
http://store.company.com:81/dir/etc.html |
失敗 | 端口不一樣 ( http:// 默認端口是80) |
http://news.company.com/dir/other.html |
失敗 | 主機不一樣 |
JSONP
JSONP的原理是:靜態資源請求不受同源策略影響。實現以下:
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://www.domain.com/a?data=1&callback=cb'
const cb = res => {
console.log(JSON.stringify(res))
}
複製代碼
CORS
CORS:跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不一樣源服務器上的指定的資源。
在各類服務端代碼實現以下:
// 根據不一樣語言規則,具體語法有所不一樣,此處以NodeJs的express爲例
//設置跨域訪問
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
複製代碼
Nginx實現以下:
server {
...
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin $http_origin;
location /file {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods $http_access_control_request_method;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Max-Age 1728000;
return 204;
}
}
...
}
複製代碼
傳輸控制協議(TCP,Transmission Control Protocol)是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議,由 IETF 的 RFC 793 定義。
Internet 協議集支持一個無鏈接的傳輸協議,該協議稱爲用戶數據報協議(UDP,User Datagram Protocol)。UDP 爲應用程序提供了一種無需創建鏈接就能夠發送封裝的 IP 數據包的方法。RFC 768 描述了 UDP。
HTTP/0.9:GET,無狀態的特色造成
HTTP/1.0:支持 POST,HEAD,添加了請求頭和響應頭,支持任何格式的文件發送,添加了狀態碼、多字符集支持、多部分發送、權限、緩存、內容編碼等
HTTP/1.1:默認長鏈接,同時 6 個 TCP 鏈接,CDN 域名分片
HTTPS:HTTP + TLS( 非對稱加密 與 對稱加密 )
HTTP/2.0:多路複用(一次 TCP 鏈接能夠處理多個請求),服務器主動推送,stream 傳輸。
HTTP/3:基於 UDP 實現了 QUIC 協議
注:RTT = Round-trip time
構建 DOM 樹、樣式計算、佈局階段、分層、繪製、分塊、光柵化和合成
建立 DOM tree
樣式計算
生成 layout tree
分層
將圖層轉換爲位圖
合成位圖並顯示在頁面中
V8 編譯 JS 代碼的過程
生成抽象語法樹(AST)和執行上下文
第一階段是分詞(tokenize),又稱爲詞法分析
第二階段是解析(parse),又稱爲語法分析
生成字節碼
字節碼就是介於 AST 和機器碼之間的一種代碼。可是與特定類型的機器碼無關,字節碼須要經過解釋器將其轉換爲機器碼後才能執行。
執行代碼
高級語言編譯器步驟:
對象在轉換類型的時候,會執行原生方法 ToPrimitive 。
其算法以下:
toSting
方法,若是此時是 原始類型 則直接返回,不然再調用valueOf
方法並返回結果;valueOf
方法,若是此時是 原始類型 則直接返回,不然再調用toString
方法並返回結果;固然,咱們能夠經過重寫Symbol.toPrimitive
來制定轉換規則,此方法在轉原始類型時調用優先級最高。
const data = {
valueOf() {
return 1;
},
toString() {
return "1";
},
[Symbol.toPrimitive]() {
return 2;
}
};
data + 1; // 3
複製代碼
對象轉換爲布爾值的規則以下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 false 。 |
Null | 返回 false 。 |
Boolean | 返回 當前參數。 |
Number | 若是參數爲+0 、-0 或NaN ,則返回 false ;其餘狀況則返回 true 。 |
String | 若是參數爲空字符串,則返回 false ;不然返回 true 。 |
Symbol | 返回 true 。 |
Object | 返回 true 。 |
對象轉換爲數字的規則以下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 NaN 。 |
Null | Return +0. |
Boolean | 若是參數爲 true ,則返回 1 ;false 則返回 +0 。 |
Number | 返回當前參數。 |
String | 先調用 ToPrimitive ,再調用 ToNumber ,而後返回結果。 |
Symbol | 拋出 TypeError 錯誤。 |
Object | 先調用 ToPrimitive ,再調用 ToNumber ,而後返回結果。 |
對象轉換爲字符串的規則以下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 "undefined" 。 |
Null | 返回 "null" 。 |
Boolean | 若是參數爲 true ,則返回 "true" ;不然返回 "false" 。 |
Number | 調用 NumberToString ,而後返回結果。 |
String | 返回 當前參數。 |
Symbol | 拋出 TypeError 錯誤。 |
Object | 先調用 ToPrimitive ,再調用 ToString ,而後返回結果。 |
this 是和執行上下文綁定的。
執行上下文:
根據優先級最高的來決定 this
最終指向哪裏。
首先,new
的方式優先級最高,接下來是 bind
這些函數,而後是 obj.foo()
這種調用方式,最後是 foo
這種調用方式,同時,箭頭函數的 this
一旦被綁定,就不會再被任何方式所改變。
三點注意:
沒有被引用的閉包會被自動回收,但還存在全局變量中,則依然會內存泄漏。
在 JavaScript 中,根據詞法做用域的規則,內部函數老是能夠訪問其外部函數中聲明的變量,當經過調用一個外部函數返回一個內部函數後,即便該外部函數已經執行結束了,可是內部函數引用外部函數的變量依然保存在內存中,咱們就把這些變量的集合稱爲閉包。好比外部函數是 foo,那麼這些變量的集合就稱爲 foo 函數的閉包。
var getNum;
function getCounter() {
var n = 1;
var inner = function() {
n++;
};
return inner;
}
getNum = getCounter();
getNum(); // 2
getNum(); // 3
getNum(); // 4
getNum(); // 5
複製代碼
對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。
函數內部定義的變量或者函數,而且定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量會被銷燬。
使用一對大括號包裹的一段代碼,好比函數、判斷語句、循環語句,甚至單獨的一個{}均可以被看做是一個塊級做用域。
詞法做用域就是指做用域是由代碼中函數聲明的位置來決定的,因此詞法做用域是靜態的做用域,經過它就可以預測代碼在執行過程當中如何查找標識符。
詞法做用域是代碼階段就決定好的,和函數是怎麼調用的沒有關係。
其實每一個 JS 對象都有 __proto__
屬性,這個屬性指向了原型。
原型也是一個對象,而且這個對象中包含了不少函數,對於 obj
來講,能夠經過 __proto__
找到一個原型對象,在該對象中定義了不少函數讓咱們來使用。
原型鏈:
Object
是全部對象的爸爸,全部對象均可以經過 __proto__
找到它Function
是全部函數的爸爸,全部函數均可以經過 __proto__
找到它prototype
是一個對象__proto__
屬性指向原型, __proto__
將對象和原型鏈接起來組成了原型鏈原始類型的賦值會完整複製變量值,而引用類型的賦值是複製引用地址。
回收調用棧內的數據:執行上下文結束且沒有被引用時,則會經過向下移動 記錄當前執行狀態的指針(稱爲 ESP) 來銷燬該函數保存在棧中的執行上下文。
回收堆裏的數據:
V8 中會把堆分爲新生代和老生代兩個區域,
新生代中存放的是生存時間短的對象,
老生代中存放的生存時間久的對象。
垃圾回收重要術語:
- 代際假說
- 大部分對象在內存中存在的時間很短
- 不死的對象,會活得更久
- 分代收集
副垃圾回收器:
主要負責新生代的垃圾回收。
這個區域不大,可是垃圾回收比較頻繁。
新生代的垃圾回收算法是 Scavenge 算法。
主要把新生代空間對半劃分爲兩個區域:對象區域,空閒區域。
當對象區域快被寫滿時,則會進行一次垃圾清理。
流程以下:
主垃圾回收器:
主垃圾回收器主要負責老生區中的垃圾回收。
除了新生區中晉升的對象,一些大的對象會直接被分配到老生區。
所以老生區中的對象有兩個特色,一個是對象佔用空間大,另外一個是對象存活時間長。
流程以下:
一旦執行垃圾回收算法,會致使 全停頓(Stop-The-World) 。
可是 V8 有 增量標記算法 。
V8 將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成。
xss:將代碼注入到網頁
csrf:跨站請求僞造。攻擊者會虛構一個後端請求地址,誘導用戶經過某些途徑發送請求。
中間人攻擊:中間人攻擊是攻擊方同時與服務端和客戶端創建起了鏈接,並讓對方認爲鏈接是安全的,可是實際上整個通訊過程都被攻擊者控制了。攻擊者不只能得到雙方的通訊信息,還能修改通訊信息。
使用轉義字符過濾 html 代碼
const escapeHTML = value => {
if (!value || !value.length) {
return value;
}
return value
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
複製代碼
過濾 SQL 代碼
const replaceSql = value => {
if (!value || !value.length) {
return value;
}
return value.replace(/select|update|delete|exec|count|'|"|=|;|>|<|%/gi, "");
};
複製代碼
預防 CSRF
預防中間人攻擊
內容安全策略 (CSP) 是一個額外的安全層,用於檢測並削弱某些特定類型的攻擊,包括跨站腳本 (XSS) 和數據注入攻擊等。不管是數據盜取、網站內容污染仍是散發惡意軟件,這些攻擊都是主要的手段。
措施以下:
Content-Security-Policy
<meta http-equiv="Content-Security-Policy">
<link rel="dns-prefetch" href="" />
<meta http-equiv="x-dns-prefetch-control" content="off|on">
Expires
Cache-Control
協商緩存就是強制緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。
Last-Modified 與 If-Modified-Since 配對。Last-Modified
把 Web 應用最後修改時間告訴客戶端,客戶端下次請求之時會把 If-Modified-Since
的值發生給服務器,服務器由此判斷是否須要從新發送資源,若是不須要則返回 304,若是有則返回 200。這對組合的缺點是隻能精確到秒,並且是根據本地打開時間來記錄的,因此會不許確。
Etag 與 If-None-Match 配對。它們沒有使用時間做爲判斷標準,而是使用了一組特徵串。Etag
把此特徵串發生給客戶端,客戶端在下次請求之時會把此特徵串做爲If-None-Match
的值發送給服務端,服務器由此判斷是否須要從新發送資源,若是不須要則返回 304,若是有則返回 200。
基礎概念:
進程:進程(英語:process),是指計算機中已運行的程序。進程曾經是分時系統的基本運做單位。
線程:線程(英語:thread)是操做系統可以進行運算調度的最小單位。大部分狀況下,它被包含在進程之中,是進程中的實際運做單位。
協程:協程(英語:coroutine),又稱微線程,是計算機程序的一類組件,推廣了協做式多任務的子程序,容許執行被掛起與被恢復。
Node 中最核心的是 v8 引擎,在 Node 啓動後,會建立 v8 的實例,這個實例是多線程的,各個線程以下:
主線程:編譯、執行代碼。
編譯/優化線程:在主線程執行的時候,能夠優化代碼。
分析器線程:記錄分析代碼運行時間,爲 Crankshaft 優化代碼執行提供依據。
垃圾回收的幾個線程。
阻塞 是指在 Node.js 程序中,其它 JavaScript 語句的執行,必須等待一個非 JavaScript 操做完成。這是由於當 阻塞 發生時,事件循環沒法繼續運行 JavaScript。
在 Node.js 中,JavaScript 因爲執行 CPU 密集型操做,而不是等待一個非 JavaScript 操做(例如 I/O)而表現不佳,一般不被稱爲 阻塞。在 Node.js 標準庫中使用 libuv 的同步方法是最經常使用的 阻塞 操做。原生模塊中也有 阻塞 方法。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
注意:每一個框被稱爲事件循環機制的一個階段。
在 Windows 和 Unix/Linux 實現之間存在細微的差別,但這對演示來講並不重要。
階段概述:
定時器 :本階段執行已經被 setTimeout()
和 setInterval()
的調度回調函數。
待定回調 :執行延遲到下一個循環迭代的 I/O 回調。
idle, prepare :僅系統內部使用。
輪詢 :檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎全部狀況下,除了關閉的回調函數,那些由計時器和 setImmediate()
調度的以外),其他狀況 node 將在適當的時候在此阻塞。
檢測 :setImmediate()
回調函數在這裏執行。
關閉的回調函數 :一些關閉的回調函數,如:socket.on('close', ...)
。
在每次運行的事件循環之間,Node.js 檢查它是否在等待任何異步 I/O 或計時器,若是沒有的話,則徹底關閉。
process.nextTick()
:它是異步 API 的一部分。從技術上講不是事件循環的一部分。無論事件循環的當前階段如何,都將在當前操做完成後處理 nextTickQueue
。這裏的一個操做被視做爲一個從底層 C/C++ 處理器開始過渡,而且處理須要執行的 JavaScript 代碼。
Libuv 是一個跨平臺的異步 IO 庫,它結合了 UNIX 下的 libev 和 Windows 下的 IOCP 的特性,最先由 Node.js 的做者開發,專門爲 Node.js 提供多平臺下的異步 IO 支持。Libuv 自己是由 C++ 語言實現的,Node.js 中的非阻塞 IO 以及事件循環的底層機制都是由 libuv 實現的。
在 Windows 環境下,libuv 直接使用 Windows 的 IOCP 來實現異步 IO。在 非 Windows 環境下,libuv 使用多線程(線程池 Thread Pool)來模擬異步 IO,這裏僅簡要提一下 libuv 中有線程池的概念,以後的文章會介紹 libuv 如何實現進程間通訊。
var New = function(Fn) {
var obj = {}; // 建立空對象
var arg = Array.prototype.slice.call(arguments, 1);
obj.__proto__ = Fn.prototype; // 將obj的原型鏈__proto__指向構造函數的原型prototype
obj.__proto__.constructor = Fn; // 在原型鏈 __proto__上設置構造函數的構造器constructor,爲了實例化Fn
Fn.apply(obj, arg); // 執行Fn,並將構造函數Fn執行obj
return obj; // 返回結果
};
複製代碼
const getType = data => {
// 獲取數據類型
const baseType = Object.prototype.toString
.call(data)
.replace(/^\[object\s(.+)\]$/g, "$1")
.toLowerCase();
const type = data instanceof Element ? "element" : baseType;
return type;
};
const isPrimitive = data => {
// 判斷是不是基本數據類型
const primitiveType = "undefined,null,boolean,string,symbol,number,bigint,map,set,weakmap,weakset".split(
","
); // 其實還有不少類型
return primitiveType.includes(getType(data));
};
const isObject = data => getType(data) === "object";
const isArray = data => getType(data) === "array";
const deepClone = data => {
let cache = {}; // 緩存值,防止循環引用
const baseClone = _data => {
let res;
if (isPrimitive(_data)) {
return data;
} else if (isObject(_data)) {
res = { ..._data };
} else if (isArray(_data)) {
res = [..._data];
}
// 判斷是否有複雜類型的數據,有就遞歸
Reflect.ownKeys(res).forEach(key => {
if (res[key] && getType(res[key]) === "object") {
// 用cache來記錄已經被複制過的引用地址。用來解決循環引用的問題
if (cache[res[key]]) {
res[key] = cache[res[key]];
} else {
cache[res[key]] = res[key];
res[key] = baseClone(res[key]);
}
}
});
return res;
};
return baseClone(data);
};
複製代碼
Function.prototype.bind2 = function(context) {
if (typeof this !== "function") {
throw new Error("...");
}
var that = this;
var args1 = Array.prototype.slice.call(arguments, 1);
var bindFn = function() {
var args2 = Array.prototype.slice.call(arguments);
var that2 = this instanceof bindFn ? this : context; // 若是當前函數的this指向的是構造函數中的this 則斷定爲new 操做。若是this是構造函數bindFn new出來的實例,那麼此處的this必定是該實例自己。
return that.apply(that2, args1.concat(args2));
};
var Fn = function() {}; // 鏈接原型鏈用Fn
// 原型賦值
Fn.prototype = this.prototype; // bindFn的prototype指向和this的prototype同樣,指向同一個原型對象
bindFn.prototype = new Fn();
return bindFn;
};
複製代碼
const curry = fn => {
if (typeof fn !== "function") {
throw Error("No function provided");
}
return function curriedFn(...args) {
if (args.length < fn.length) {
return function() {
return curriedFn.apply(null, args.concat([].slice.call(arguments)));
};
}
return fn.apply(null, args);
};
};
複製代碼
// 來源於 https://github.com/bailnl/promise/blob/master/src/promise.js
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;
const isFunction = fn => typeof fn === "function";
const isObject = obj => obj !== null && typeof obj === "object";
const noop = () => {};
const nextTick = fn => setTimeout(fn, 0);
const resolve = (promise, x) => {
if (promise === x) {
reject(promise, new TypeError("You cannot resolve a promise with itself"));
} else if (x && x.constructor === Promise) {
if (x._stauts === PENDING) {
const handler = statusHandler => value => statusHandler(promise, value);
x.then(handler(resolve), handler(reject));
} else if (x._stauts === FULFILLED) {
fulfill(promise, x._value);
} else if (x._stauts === REJECTED) {
reject(promise, x._value);
}
} else if (isFunction(x) || isObject(x)) {
let isCalled = false;
try {
const then = x.then;
if (isFunction(then)) {
const handler = statusHandler => value => {
if (!isCalled) {
statusHandler(promise, value);
}
isCalled = true;
};
then.call(x, handler(resolve), handler(reject));
} else {
fulfill(promise, x);
}
} catch (e) {
if (!isCalled) {
reject(promise, e);
}
}
} else {
fulfill(promise, x);
}
};
const reject = (promise, reason) => {
if (promise._stauts !== PENDING) {
return;
}
promise._stauts = REJECTED;
promise._value = reason;
invokeCallback(promise);
};
const fulfill = (promise, value) => {
if (promise._stauts !== PENDING) {
return;
}
promise._stauts = FULFILLED;
promise._value = value;
invokeCallback(promise);
};
const invokeCallback = promise => {
if (promise._stauts === PENDING) {
return;
}
nextTick(() => {
while (promise._callbacks.length) {
const {
onFulfilled = value => value,
onRejected = reason => {
throw reason;
},
thenPromise
} = promise._callbacks.shift();
let value;
try {
value = (promise._stauts === FULFILLED ? onFulfilled : onRejected)(
promise._value
);
} catch (e) {
reject(thenPromise, e);
continue;
}
resolve(thenPromise, value);
}
});
};
class Promise {
static resolve(value) {
return new Promise((resolve, reject) => resolve(value));
}
static reject(reason) {
return new Promise((resolve, reject) => reject(reason));
}
constructor(resolver) {
if (!(this instanceof Promise)) {
throw new TypeError(
`Class constructor Promise cannot be invoked without 'new'`
);
}
if (!isFunction(resolver)) {
throw new TypeError(`Promise resolver ${resolver} is not a function`);
}
this._stauts = PENDING;
this._value = undefined;
this._callbacks = [];
try {
resolver(value => resolve(this, value), reason => reject(this, reason));
} catch (e) {
reject(this, e);
}
}
then(onFulfilled, onRejected) {
const thenPromise = new this.constructor(noop);
this._callbacks = this._callbacks.concat([
{
onFulfilled: isFunction(onFulfilled) ? onFulfilled : void 0,
onRejected: isFunction(onRejected) ? onRejected : void 0,
thenPromise
}
]);
invokeCallback(this);
return thenPromise;
}
catch(onRejected) {
return this.then(void 0, onRejected);
}
}
複製代碼
const debounce = (fn = {}, wait = 50, immediate) => {
let timer;
return function() {
if (immediate) {
fn.apply(this, arguments);
}
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, wait);
};
};
複製代碼
var throttle = (fn = {}, wait = 0) => {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
};
};
複製代碼
const instanceOf = (left, right) => {
let proto = left.__proto__;
let prototype = right.prototype;
while (true) {
if (proto === null) {
return false;
} else if (proto === prototype) {
return true;
}
proto = proto.__proto__;
}
};
複製代碼
instanceof
運算符用來檢測 constructor.prototype
是否存在於參數 object
的原型鏈上。
typeof
操做符返回一個字符串,表示未經計算的操做數的類型。
在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標籤和實際數據值表示的。對象的類型標籤是 0。因爲 null
表明的是空指針(大多數平臺下值爲 0x00),所以,null 的類型標籤是 0,typeof null
也所以返回 "object"
。
遞歸(英語:Recursion),又譯爲遞迴,在數學與計算機科學中,是指在函數的定義中使用函數自身的方法。
例如:
大雄在房裏,用時光電視看着將來的狀況。電視畫面中的那個時候,他正在房裏,用時光電視,看着將來的狀況。電視畫面中的電視畫面的那個時候,他正在房裏,用時光電視,看着將來的狀況……
簡單來講,就是 無限套娃
咱們以斐波那契數列(Fibonacci sequence)爲例,看看輸入結果會爲正無窮的值的狀況下,各類遞歸的狀況。
首先是普通版
const fib1 = n => {
if (typeof n !== "number") {
throw new Error("..");
}
if (n < 2) {
return n;
}
return fib1(n - 1) + fib1(n - 2);
};
複製代碼
從上面的代碼分析,咱們不難發現,在fib1
裏,JS 會不停建立執行上下文,壓入棧內,並且在得出結果前不會銷燬,因此數大了以後容易爆棧。
因此咱們能夠對其進行優化,就是利用 尾調用 進行優化。
尾調用是指函數的最後一步只返回一個純函數的調用,而沒有別的數據佔用引用。代碼以下:
const fib2 = (n, a = 0, b = 1) => {
if (typeof n !== "number") {
throw new Error("..");
}
if (n === 0) {
return a;
}
return fib2(n - 1, b, a + b);
};
複製代碼
不過很遺憾,在 Chrome 83.0.4103.61 裏仍是會爆。
而後咱們還有備忘錄遞歸法,就是另外申請空間去存儲每次遞歸的值,是個自頂向下的算法。
惋惜,仍是掛了。
不過在一些遞歸問題上,咱們還能夠利用動態規劃(Dynamic programming,簡稱 DP)來解決。
動態規劃是算法裏比較難掌握的一個概念之一,可是基本能用遞歸來解決的問題,都能用動態規劃來解決。
動態規劃背後的基本思想很是簡單。大體上,若要解一個給定問題,咱們須要解其不一樣部分(即子問題),再根據子問題的解以得出原問題的解。
跟備忘錄遞歸恰好相反,是自底向上的算法。具體代碼以下:
const fib3 = n => {
if (typeof n !== "number") {
throw new Error("..");
}
if (n < 2) {
return n;
}
let a = 0;
let b = 1;
while (n--) {
[a, b] = [b, a + b];
}
return a;
};
複製代碼
效果很好,正確輸出了正無窮~
若是你喜歡探討技術,或者對本文有任何的意見或建議,很是歡迎加魚頭微信好友一塊兒探討,固然,魚頭也很是但願能跟你一塊兒聊生活,聊愛好,談天說地。 魚頭的微信號是:krisChans95 掃碼公衆號,回覆『面試資料』能夠獲取約200M前端高質量面試資料,千萬不要錯過哦。