說說js中的詞法做用域css
js中只有詞法做用域,也就是說在定義時而不是執行時肯定做用域。例如:html
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();<br>//1
複製代碼
注意: with和eval能夠修改詞法做用域node
什麼是閉包react
《深刻淺出nodejs》中對閉包的定義:web
在js中,實現外部做用域訪問內部做用域中變量的方法叫作「閉包」。算法
說說js的垃圾回收(GC)json
v8的垃圾回收策略主要基於分代式垃圾回收機制。將內存分爲新生代和老生代,分別採用不一樣的算法。設計模式
Scavenge爲新生代採用的算法,是一種採用複製的方式實現的垃圾回收算法。它將內存分爲from和to兩個空間。每次gc,會將from空間的存活對象複製到to空間。而後兩個空間角色對換(又稱反轉)。
該算法是犧牲空間換時間,因此適合新生代,由於它的對象生存週期較短。api
老生代中對象存活時間較長,不適合Scavenge算法。
Mark-Sweep是標記清除的意思。Scavenge是隻複製存活對象,而Mark-Sweep是隻清除死亡對象。該算法分爲兩個步驟:數組
Mark-Sweep存在一個問題,清除死亡對象後會形成內存空間不連續,若是這時候再分配一個大對象,全部的空間碎片都沒法完成這次分配,就會形成提早觸發gc。這時候v8會使用Mark-Compact算法。
Mark-Copact是標記整理的意思。它會在標記完成以後將活着的對象往一端移動,移動完成後直接清理掉邊界外的內存。由於存在整理過程,因此它的速度慢於Mark-Sweep,node中主要採用Mark-Sweep。
爲了不出現Javascript應用邏輯與垃圾回收器看到的狀況不一致,垃圾回收時應用邏輯會停下來。這種行爲被成爲全停頓(stop-the-world)。這對老生代影響較大。
Incremental Marking稱爲增量標記,也就是拆分爲許多小的「步進」,每次作完一「步進」,就讓Javascript執行一下子,垃圾回收與應用邏輯交替執行。
採用Incremental Marking後,gc的最大停頓時間較少到原來的 1 / 6 左右。
➜ ~ node
> process.memoryUsage() //node進程內存使用
{ rss: 27054080, // 進程常駐內存
heapTotal: 7684096, // 已申請到的堆內存
heapUsed: 4850344, // 當前使用的堆內存
external: 9978 // 堆外內存(不是經過v8分配的內存)
> os.totalmem() //系統總內存
17179869184
> os.freemem() //系統閒置內存
3239858176
複製代碼
說說你瞭解的設計模式
在js中事件模型就至關於傳統的發佈訂閱模式,具體實現參考實現一個node中的EventEmiter
定義: 定義一系列算法,把它們一個個封裝起來,而且使它們能夠相互替換。
const strategies = {
isNoEmpty: function(value, errorMsg){
if(value.trim() === ''){
return errorMsg
}
},
maxLength: function(value, errorMsg, len) {
if(value.trim() > len) {
return errorMsg
}
}
}
class Validator {
constructor() {
this.catch = [];
}
add(value, rule, errorMsg, ...others) {
this.catch.push(function() {
return strategies[rule].apply(this, [value, errorMsg, ...others]);
});
}
start() {
for (let i = 0, validatorFunc; (validatorFunc = this.catch[i++]); ) {
let msg = validatorFunc();
if (msg) {
return msg;
}
}
}
}
//使用
const validatorFunc = function() {
const validator = new Validator();
validator.add(username, 'isNoEmpty', '用戶名不能爲空');
validator.add(password, 'isNoEmpty', '密碼不能爲空');
const USERNAME_LEN = PASSWORD_LEN = 10;
validator.add(username, 'maxLength', `用戶名不能超過${USERNAME_LEN}個字`, USERNAME_LEN);
validator.add(password, 'isNoEmpty', `密碼不能爲空${PASSWORD_LEN}個字`, PASSWORD_LEN);
let msg = validator.start();
if(msg) {
return msg;
}
}
複製代碼
應用場景: 有時候咱們要向某些對象發送請求,但不知道請求的接收者是誰,也不知道請求的操做是什麼,此時但願以一種鬆耦合的方式來設計軟件,使得請求的發送者和接收者可以消除彼此的耦合關係。
class MoveCommand {
constructor(reciever, pos) {
this.reciever = reciever;
this.pos = pos;
this.oldPos = null;
}
excute() {
this.reciever.start("left", this.pos, 1000);
this.reciever.getPos();
}
undo() {
this.reciever.start("left", this.oldPos, 1000);
}
}
複製代碼
ES6 模塊與 CommonJS 模塊的差別
CommonJS輸出的是值的拷貝,ES6模塊輸出的是值的引用。
也就是說CommonJS引用後改變模塊內變量的值,其餘引用模塊不會改變,而ES6模塊會改變。
CommonJS是運行時加載,ES6模塊是編譯時輸出接口。
之因此Webpack的Tree Shaking是基於ES6的,就是由於ES6在編譯的時候就能肯定依賴。由於使用babel-preset-2015這個預設默認是會把ES6模塊編譯爲CommonJS的,因此想使用Tree Shaking還須要手動修改這個預設。
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [['babel-preset-es2015', {modules: false}]],
}
}
}
]
}
複製代碼
async函數實現原理
async函數是基於generator實現,因此涉及到generator相關知識。在沒有async函數以前,一般使用co庫來執行generator,因此經過co咱們也能模擬async的實現。
function Asyncfn() {
return co(function*() {
//.....
});
}
function co(gen) {
return new Promise((resolve, reject) => {
const fn = gen();
function next(data) {
let { value, done } = fn.next(data);
if (done) return resolve(value);
Promise.resolve(value).then(res => {
next(res);
}, reject);
}
next();
});
}
複製代碼
說說瀏覽器和node中的事件循環(EventLoop)
如圖:瀏覽器中相對簡單,共有兩個事件隊列,當主線程空閒時會清空Microtask queue(微任務隊列)依次執行Task Queue(宏任務隊列)中的回調函數,每執行完一個以後再清空Microtask queue。
「當前執行棧」 -> 「micro-task」 -> 「task queue中取一個回調」 -> 「micro-task」 -> ... (不斷消費task queue) -> 「micro-task」
node中機制和瀏覽器有一些差別。node中的task queue是分爲幾個階段,清空micro-task是在一個階段結束以後(瀏覽器中是每個任務結束以後),各個階段以下:
┌───────────────────────┐ ┌─>│ timers │<————— 執行 setTimeout()、setInterval() 的回調 │ └──────────┬────────────┘ | |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ │ │ pending callbacks │<————— 執行由上一個 Tick 延遲下來的 I/O 回調(待完善,可忽略) │ └──────────┬────────────┘ | |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ │ │ idle, prepare │<————— 內部調用(可忽略) │ └──────────┬────────────┘ | |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調 | | ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ - (執行幾乎全部的回調,除了 close callbacks 以及 timers 調度的回調和 setImmediate() 調度的回調,在恰當的時機將會阻塞在此階段) │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ | | | | | └───────────────┘ | |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調 | ┌──────────┴────────────┐ │ │ check │<————— setImmediate() 的回調將會在這個階段執行 │ └──────────┬────────────┘ | |<-- 執行全部 Next Tick Queue 以及 MicroTask Queue 的回調 │ ┌──────────┴────────────┐ └──┤ close callbacks │<————— socket.on('close', ...) └───────────────────────┘
這裏咱們主要關注其中的3個階段:timer、poll和check,其中poll隊列相對複雜:
輪詢 階段有兩個重要的功能:
一、計算應該阻塞和輪詢 I/O 的時間。
二、而後,處理 輪詢 隊列裏的事件。
當事件循環進入 輪詢 階段且 沒有計劃計時器時 ,將發生如下兩種狀況之一:
一、若是輪詢隊列不是空的,事件循環將循環訪問其回調隊列並同步執行它們,直到隊列已用盡,或者達到了與系統相關的硬限制。
二、若是輪詢隊列是空的,還有兩件事發生:
a、若是腳本已按 setImmediate() 排定,則事件循環將結束 輪詢 階段,並繼續 check階段以執行這些計劃腳本。
b、若是腳本 還沒有 按 setImmediate()排定,則事件循環將等待回調添加到隊列中,而後當即執行。
一旦輪詢隊列爲空,事件循環將檢查已達到時間閾值的計時器。若是一個或多個計時器已準備就緒,則事件循環將繞回計時器階段以執行這些計時器的回調。
細節請參考The Node.js Event Loop, Timers, and process.nextTick()
中文:Node.js 事件循環,定時器和 process.nextTick()
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(function() {
console.log("promise2");
});
}, 0);
複製代碼
在瀏覽器中的順序是:timer1 -> promise1 -> timer2 -> pormise2
node中順序是: timer1 -> timer2 -> promise1 -> promise2
這道題目很好的說明了node中的micro-task是在一個階段的任務執行完以後才清空的。
實現一個node中的EventEmiter
簡單實現:
class EventsEmiter {
constructor() {
this.events = {};
}
on(type, fn) {
const events = this.events;
if (!events[type]) {
events[type] = [fn];
} else {
events[type].push(fn);
}
}
emit(type, ...res) {
const events = this.events;
if (events[type]) {
events[type].forEach(fn => fn.apply(this, res));
}
}
remove(type, fn) {
const events = this.events;
if (events[type]) {
events[type] = events[type].filer(lisener => lisener !== fn);
}
}
}
複製代碼
實現一個node中util模塊的promisify方法
let fs = require("fs");
let read = fs.readFile;
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
};
}
// 回調用法
// read("./test.json", (err, data) => {
// if (err) {
// console.error("err", err);
// }
// console.log("data", data.toString());
// });
// promise用法
let readPromise = promisify(read);
readPromise("./test.json").then(res => {
console.log("data", res.toString());
});
複製代碼
如何實現一個自定義流
根據所建立的流類型,新的流類必須實現一個或多個特定的方法,以下圖所示:
用例 | 類 | 需實現的方法 |
---|---|---|
只讀流 | Readable |
_read |
只寫流 | Writable |
_write , _writev , _final |
可讀可寫流 | Duplex |
_read , _write , _writev , _final |
對寫入的數據進行操做,而後讀取結果 | Transform |
_transform , _flush , _final |
以雙工流爲例:
const { Duplex } = require('stream');
class Myduplex extends Duplex {
constructor(arr, opt) {
super(opt);
this.arr = arr
this.index = 0
}
//實現可讀流部分
_read(size) {
this.index++
if(this.index === 3) {
this.push(null)
} else {
this.push(this.index.toString())
}
}
//實現可寫流
_write(chunk, encoding, callback) {
this.arr.push(chunk.toString())
callback()
}
}
複製代碼
更多內容能夠參考個人另外一篇文章:說說node中可讀流和可寫流 和 nodejs官網
性能優化之dns-prefetch、prefetch、preload、defer、async
域名轉化爲ip是一個比較耗時的過程,dns-prefetch能讓瀏覽器空閒的時候幫你作這件事。尤爲大型網站會使用多域名,這時候更加須要dns預取。
//來自百度首頁
<link rel="dns-prefetch" href="//m.baidu.com">
複製代碼
prefetch通常用來預加載可能使用的資源,通常是對用戶行爲的一種判斷,瀏覽器會在空閒的時候加載prefetch的資源。
<link rel="prefetch" href="http://www.example.com/">
複製代碼
和prefetch不一樣,prefecth一般是加載接下來可能用到的頁面資源,而preload是加載當前頁面要用的腳本、樣式、字體、圖片等資源。因此preload不是空閒時加載,它的優先級更強,而且會佔用http請求數量。
<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"
複製代碼
as值包括
//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
複製代碼
defer和async都是異步(並行)加載資源,不一樣點是async是加載完當即執行,而defer是加載完不執行,等到全部元素解析完再執行,也就是DOMContentLoaded事件觸發以前。
由於async加載的資源是加載完執行,因此它比不能保證順序,而defer會按順序執行腳本。
說說react性能優化
舉例:下面是antd-design-mobile的Modal組件中對的內部蒙層組件的處理
import * as React from "react";
export interface lazyRenderProps {
style: {};
visible?: boolean;
className?: string;
}
export default class LazyRender extends React.Component<lazyRenderProps, any> {
shouldComponentUpdate(nextProps: lazyRenderProps) {
return !!nextProps.visible;
}
render() {
const props: any = { ...this.props };
delete props.visible;
return <div {...props} />;
}
}
複製代碼
像上面這種只比較了一個visible屬性,而且它是string類型,若是是一個object類型那麼就不能直接比較了,這時候使用immutable庫更好一些。
immutable優點:
解決方案:seamless-immutable seamless-immutable這個庫沒有完整實現Persistent Data Structure,而是使用了Object.defineProperty擴展了JS的Object和Array對象,因此保持了相同的Api,同時庫的代碼量更少,壓縮後大約2k
文檔中已經強調,key須要保證在當前的做用域中惟一,不要使用當前循環的index(尤爲在長列表中)。
參考 reactjs.org/docs/reconc…
說說瀏覽器渲染流程
由一個主線程和多個web worder線程組成,web worker線程不能操做dom
用於解析html生成DOM樹,解析css生成CSSOM,佈局layout、繪製paint。迴流和重繪依賴該線程
當事件觸發時,該線程將事件的回調函數放入callback queue(任務隊列)中,等待js引擎線程處理
setTimeout和setInterval由該線程來記時,記時結束,將回調函數放入任務隊列
每有一個http請求就開一個該線程,每當檢測到狀態變動就會產生一個狀態變動事件,若是這個事件由對應的回掉函數,將這個函數放入任務隊列
用於輪詢監放任務隊列
當初始的HTML文檔被徹底加載和解析完成(script腳本執行完,所屬的script腳本以前的樣式表加載解析完成)以後,DOMContentLoaded事件被觸發
全部資源加載完成觸發window的onload事件
說說http2.0
http2.0是對SPDY協議的一個升級版。和http1.0相比主要有如下特性:
詳細可參考: HTTP----HTTP2.0新特性
實現一個reduce方法
注意邊界條件:一、數組長度爲0,而且reduce沒有傳入初始參數時,拋出錯誤。二、reduce有返回值。
Array.prototype.myReduce = function(fn, initial) {
if (this.length === 0 && !initial) {
throw new Error("no initial and array is empty");
}
let start = 1;
let pre = this[0];
if (initial) {
start = 0;
pre = initial;
}
for (let i = start; i < this.length; i++) {
let current = this[i];
pre = fn.call(this, pre, current, i);
}
return pre;
};
複製代碼
實現一個promise.all方法,要求保留錯誤而且併發數爲3
標準的all方法是遇到錯誤會當即將promise置爲失敗態,並觸發error回調。保留錯誤的定義爲:promise遇到錯誤保存在返回的結果中。
function promiseall(promises) {
return new Promise(resolve => {
let result = [];
let flag = 0;
let taskQueue = promises.slice(0, 3); //任務隊列,初始爲最大併發數3
let others = promises.slice(3); //排隊的任務
taskQueue.forEach((promise, i) => {
singleTaskRun(promise, i);
});
let i = 3; //新的任務從索引3開始
function next() {
if (others.length === 0) {
return;
}
const newTask = others.shift();
singleTaskRun(newTask, i++);
}
function singleTaskRun(promise, i) {
promise
.then(res => {
check();
result[i] = res;
next();
})
.catch(err => {
check();
result[i] = err;
next();
});
}
function check() {
flag++;
if (flag === promises.length) {
resolve(result);
}
}
});
}
複製代碼
測試代碼:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("1");
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("2");
}, 1500);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("3");
}, 2000);
});
let p4 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("4");
}, 2500);
});
let p_e = new Promise((resolve, reject) => {
// throw new Error("出錯");
reject("錯誤");
});
let p5 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("5");
}, 5000);
});
let all = promiseall([p_e, p1, p3, p2, p4, p5]);
all.then(
data => {
console.log("data", data); // [ '錯誤', '1', '3', '2', '4', '5' ]
}
);
複製代碼
不用遞歸函數求一個二叉樹的高度
先看一下遞歸的實現(二叉樹的深度優先遍歷):
function getBinaryTreeHeigth(node) {
let maxDeep = 0;
function next(n, deep) {
deep++;
if (n.l) {
let newDeep = next(n.l, deep);
if (newDeep > maxDeep) {
maxDeep = newDeep;
}
}
if (n.r) {
let newDeep = next(n.r, deep);
if (newDeep > maxDeep) {
maxDeep = newDeep;
}
}
return deep;
}
next(node, 0);
return maxDeep;
}
function Node(v, l, r) {
this.v = v;
this.l = l;
this.r = r;
}
複製代碼
非遞歸的實現(二叉樹的廣度優先遍歷):
function getBinaryTreeHeigth(node) {
if (!node) {
return 0;
}
const queue = [node];
let deep = 0;
while (queue.length) {
deep++;
for (let i = 0; i < queue.length; i++) {
const cur = queue.pop();
if (cur.l) {
queue.unshift(cur.l);
}
if (cur.r) {
queue.unshift(cur.r);
}
}
}
return deep;
}
function Node(v, l, r) {
this.v = v;
this.l = l;
this.r = r;
}
複製代碼
js中求兩個大數相加
給定兩個以字符串形式表示的非負整數 num1 和 num2,返回它們的和,仍用字符串表示。
輸入:num1 = '1234', num2 = '987'
輸出:'2221'
function bigIntAdd(str1, str2) {
let result = [];
let ary1 = str1.split("");
let ary2 = str2.split("");
let flag = false; //是否進位
while (ary1.length || ary2.length) {
let result_c = sigle_pos_add(ary1.pop(), ary2.pop());
if (flag) {
result_c = result_c + 1;
}
result.unshift(result_c % 10);
if (result_c >= 10) {
flag = true;
} else {
flag = false;
}
}
if(flag) {
result.unshift('1');
}
return result.join("");
}
function sigle_pos_add(str1_c, str2_c) {
let l = (r = 0);
if (str1_c) {
l = Number(str1_c);
}
if (str2_c) {
r = Number(str2_c);
}
return l + r;
}
複製代碼
測試代碼:
const str1 = "1234";
const str2 = "987654321";
const str3 = "4566786445677555";
const str4 = "987";
console.log(bigIntAdd(str1, str4)) //'2221'
console.log(bigIntAdd(str2, str3)) //'4566787433331876'
複製代碼
實現一個數組隨機打亂算法
function disOrder(ary) {
for (let i = 0; i < ary.length; i++) {
let randomIndex = Math.floor(Math.random() * ary.length);
swap(ary, i, randomIndex);
}
}
function swap(ary, a, b) {
let temp = ary[a];
ary[a] = ary[b];
ary[b] = temp;
}
let ary = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
disOrder(ary);
console.log(ary);
複製代碼
給數字增長「逗號」分隔
輸入: '"123456789.012"' 輸出:123,456,789.012
function parseNumber(num) {
if (!num) return "";
return num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,");
}
複製代碼
function formatNumber(num) {
if (!num) return "";
let [int, float] = num.split(".");
let intArr = int.split("");
let result = [];
let i = 0;
while (intArr.length) {
if (i !== 0 && i % 3 === 0) {
result.unshift(intArr.pop() + ",");
} else {
result.unshift(intArr.pop());
}
i++;
}
return result.join("") + "." + (float ? float : "");
}
複製代碼