實現一個call
call做了什麼:
- 將函式設為物件的屬性
- 執行&刪除這個函式
- 指定this到函式並傳入給定引數執行函式
- 如果不傳入引數,預設指向為 window
// 模擬 call bar.mycall(null);
//實現一個call方法:
Function.prototype.myCall = function(context) {
//此處沒有考慮context非object情況
context.fn = this;
let args = [];
for (let i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
}
context.fn(...args);
let result = context.fn(...args);
delete context.fn;
return result;
};
手寫常見排序
氣泡排序
氣泡排序的原理如下,從第一個元素開始,把當前元素和下一個索引元素進行比較。如果當前元素大,那麼就交換位置,重複操作直到比較到最後一個元素,那麼此時最後一個元素就是該陣列中最大的數。下一輪重複以上操作,但是此時最後一個元素已經是最大數了,所以不需要再比較最後一個元素,只需要比較到 length - 1
的位置。
function bubbleSort(list) {
var n = list.length;
if (!n) return [];
for (var i = 0; i < n; i++) {
// 注意這裡需要 n - i - 1
for (var j = 0; j < n - i - 1; j++) {
if (list[j] > list[j + 1]) {
var temp = list[j + 1];
list[j + 1] = list[j];
list[j] = temp;
}
}
}
return list;
}
快速排序
快排的原理如下。隨機選取一個陣列中的值作為基準值,從左至右取值與基準值對比大小。比基準值小的放陣列左邊,大的放右邊,對比完成後將基準值和第一個比基準值大的值交換位置。然後將陣列以基準值的位置分為兩部分,繼續遞迴以上操作
ffunction quickSort(arr) {
if (arr.length<=1){
return arr;
}
var baseIndex = Math.floor(arr.length/2);//向下取整,選取基準點
var base = arr.splice(baseIndex,1)[0];//取出基準點的值,
// splice 透過刪除或替換現有元素或者原地新增新的元素來修改陣列,並以陣列形式返回被修改的內容。此方法會改變原陣列。
// slice方法返回一個新的陣列物件,不會更改原陣列
//這裡不能直接base=arr[baseIndex],因為base代表的每次都刪除的那個數
var left=[];
var right=[];
for (var i = 0; i<arr.length; i++){
// 這裡的length是變化的,因為splice會改變原陣列。
if (arr[i] < base){
left.push(arr[i]);//比基準點小的放在左邊陣列,
}
}else{
right.push(arr[i]);//比基準點大的放在右邊陣列,
}
return quickSort(left).concat([base],quickSort(right));
}
選擇排序
function selectSort(arr) {
// 快取陣列長度
const len = arr.length;
// 定義 minIndex,快取當前區間最小值的索引,注意是索引
let minIndex;
// i 是當前排序區間的起點
for (let i = 0; i < len - 1; i++) {
// 初始化 minIndex 為當前區間第一個元素
minIndex = i;
// i、j分別定義當前區間的上下界,i是左邊界,j是右邊界
for (let j = i; j < len; j++) {
// 若 j 處的資料項比當前最小值還要小,則更新最小值索引為 j
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 如果 minIndex 對應元素不是目前的頭部元素,則交換兩者
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
}
return arr;
}
// console.log(selectSort([3, 6, 2, 4, 1]));
插入排序
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let j = i;
let target = arr[j];
while (j > 0 && arr[j - 1] > target) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = target;
}
return arr;
}
// console.log(insertSort([3, 6, 2, 4, 1]));
實現一個JS函式柯里化
預先處理的思想,利用閉包的機制
- 柯里化的定義:接收一部分引數,返回一個函式接收剩餘引數,接收足夠引數後,執行原函式
- 函式柯里化的主要作用和特點就是
引數複用
、提前返回
和延遲執行
- 柯里化把多次傳入的引數合併,柯里化是一個高階函式
- 每次都返回一個新函式
- 每次入參都是一個
當柯里化函式接收到足夠引數後,就會執行原函式,如何去確定何時達到足夠的引數呢?
有兩種思路:
- 透過函式的
length
屬性,獲取函式的形參個數,形參的個數就是所需的引數個數 - 在呼叫柯里化工具函式時,手動指定所需的引數個數
將這兩點結合一下,實現一個簡單 curry
函式
通用版
// 寫法1
function curry(fn, args) {
var length = fn.length;
var args = args || [];
return function(){
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this,fn,newArgs);
}else{
return fn.apply(this,newArgs);
}
}
}
// 寫法2
// 分批傳入引數
// redux 原始碼的compose也是用了類似柯里化的操作
const curry = (fn, arr = []) => {// arr就是我們要收集每次呼叫時傳入的引數
let len = fn.length; // 函式的長度,就是引數的個數
return function(...args) {
let newArgs = [...arr, ...args] // 收集每次傳入的引數
// 如果傳入的引數個數等於我們指定的函式引數個數,就執行指定的真正函式
if(newArgs.length === len) {
return fn(...newArgs)
} else {
// 遞迴收集引數
return curry(fn, newArgs)
}
}
}
// 測試
function multiFn(a, b, c) {
return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4)
ES6寫法
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args])
// 測試
let curryTest=curry((a,b,c,d)=>a+b+c+d)
curryTest(1,2,3)(4) //返回10
curryTest(1,2)(4)(3) //返回10
curryTest(1,2)(3,4) //返回10
// 柯里化求值
// 指定的函式
function sum(a,b,c,d,e) {
return a + b + c + d + e
}
// 傳入指定的函式,執行一次
let newSum = curry(sum)
// 柯里化 每次入參都是一個引數
newSum(1)(2)(3)(4)(5)
// 偏函式
newSum(1)(2)(3,4,5)
// 柯里化簡單應用
// 判斷型別,引數多少個,就執行多少次收集
function isType(type, val) {
return Object.prototype.toString.call(val) === `[object ${type}]`
}
let newType = curry(isType)
// 相當於把函式引數一個個傳了,把第一次先快取起來
let isString = newType('String')
let isNumber = newType('Number')
isString('hello world')
isNumber(999)
實現bind方法
bind
的實現對比其他兩個函式略微地複雜了一點,涉及到引數合併(類似函式柯里化),因為bind
需要返回一個函式,需要判斷一些邊界問題,以下是bind
的實現
bind
返回了一個函式,對於函式來說有兩種方式呼叫,一種是直接呼叫,一種是透過new
的方式,我們先來說直接呼叫的方式- 對於直接呼叫來說,這裡選擇了
apply
的方式實現,但是對於引數需要注意以下情況:因為bind
可以實現類似這樣的程式碼f.bind(obj, 1)(2)
,所以我們需要將兩邊的引數拼接起來 - 最後來說透過
new
的方式,對於new
的情況來說,不會被任何方式改變this
,所以對於這種情況我們需要忽略傳入的this
簡潔版本
- 對於普通函式,繫結
this
指向 - 對於建構函式,要保證原函式的原型物件上的屬性不能丟失
Function.prototype.myBind = function(context = window, ...args) {
// this表示呼叫bind的函式
let self = this;
//返回了一個函式,...innerArgs為實際呼叫時傳入的引數
let fBound = function(...innerArgs) {
//this instanceof fBound為true表示建構函式的情況。如new func.bind(obj)
// 當作為建構函式時,this 指向例項,此時 this instanceof fBound 結果為 true,可以讓例項獲得來自繫結函式的值
// 當作為普通函式時,this 指向 window,此時結果為 false,將繫結函式的 this 指向 context
return self.apply(
this instanceof fBound ? this : context,
args.concat(innerArgs)
);
}
// 如果繫結的是建構函式,那麼需要繼承建構函式原型屬性和方法:保證原函式的原型物件上的屬性不丟失
// 實現繼承的方式: 使用Object.create
fBound.prototype = Object.create(this.prototype);
return fBound;
}
// 測試用例
function Person(name, age) {
console.log('Person name:', name);
console.log('Person age:', age);
console.log('Person this:', this); // 建構函式this指向例項物件
}
// 建構函式原型的方法
Person.prototype.say = function() {
console.log('person say');
}
// 普通函式
function normalFun(name, age) {
console.log('普通函式 name:', name);
console.log('普通函式 age:', age);
console.log('普通函式 this:', this); // 普通函式this指向繫結bind的第一個引數 也就是例子中的obj
}
var obj = {
name: 'poetries',
age: 18
}
// 先測試作為建構函式呼叫
var bindFun = Person.myBind(obj, 'poetry1') // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say
// 再測試作為普通函式呼叫
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined
bindNormalFun(12) // 普通函式name: poetry2 普通函式 age: 12 普通函式 this: {name: 'poetries', age: 18}
注意:bind
之後不能再次修改this
的指向,bind
多次後執行,函式this
還是指向第一次bind
的物件
Promise實現
基於Promise
封裝Ajax
- 返回一個新的
Promise
例項 - 建立
HMLHttpRequest
非同步物件 - 呼叫
open
方法,開啟url
,與伺服器建立連結(傳送前的一些處理) - 監聽
Ajax
狀態資訊 如果
xhr.readyState == 4
(表示伺服器響應完成,可以獲取使用伺服器的響應了)xhr.status == 200
,返回resolve
狀態xhr.status == 404
,返回reject
狀態
xhr.readyState !== 4
,把請求主體的資訊基於send
傳送給伺服器
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject('請求出錯')
}
}
}
xhr.send() //傳送hppt請求
})
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
實現Object.freeze
Object.freeze
凍結一個物件,讓其不能再新增/刪除屬性,也不能修改該物件已有屬性的可列舉性、可配置可寫性,也不能修改已有屬性的值和它的原型屬性,最後返回一個和傳入引數相同的物件
function myFreeze(obj){
// 判斷引數是否為Object型別,如果是就封閉物件,迴圈遍歷物件。去掉原型屬性,將其writable特性設定為false
if(obj instanceof Object){
Object.seal(obj); // 封閉物件
for(let key in obj){
if(obj.hasOwnProperty(key)){
Object.defineProperty(obj,key,{
writable:false // 設定只讀
})
// 如果屬性值依然為物件,要透過遞迴來進行進一步的凍結
myFreeze(obj[key]);
}
}
}
}
參考 前端進階面試題詳細解答
實現一個雙向繫結
defineProperty 版本
// 資料
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 資料劫持
Object.defineProperty(data, 'text', {
// 資料變化 --> 修改檢視
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 檢視更改 --> 資料變化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
proxy 版本
// 資料
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 資料劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 資料變化 --> 修改檢視
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 檢視更改 --> 資料變化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
實現深複製
簡潔版本
簡單版:
const newObj = JSON.parse(JSON.stringify(oldObj));
侷限性:
- 他無法實現對函式 、RegExp等特殊物件的克隆
- 會拋棄物件的
constructo
r,所有的建構函式會指向Object
- 物件有迴圈引用,會報錯
面試簡版
function deepClone(obj) {
// 如果是 值型別 或 null,則直接return
if(typeof obj !== 'object' || obj === null) {
return obj
}
// 定義結果物件
let copy = {}
// 如果物件是陣列,則定義結果陣列
if(obj.constructor === Array) {
copy = []
}
// 遍歷物件的key
for(let key in obj) {
// 如果key是物件的自有屬性
if(obj.hasOwnProperty(key)) {
// 遞迴呼叫深複製方法
copy[key] = deepClone(obj[key])
}
}
return copy
}
呼叫深複製方法,若屬性為值型別,則直接返回;若屬性為引用型別,則遞迴遍歷。這就是我們在解這一類題時的核心的方法。
進階版
- 解決複製迴圈引用問題
- 解決複製對應原型問題
// 遞迴複製 (型別判斷)
function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合適一點
// null 和 undefiend 是不需要複製的
if(value == null){ return value;}
if(value instanceof RegExp) { return new RegExp(value) }
if(value instanceof Date) { return new Date(value) }
// 函式是不需要複製
if(typeof value != 'object') return value;
let obj = new value.constructor(); // [] {}
// 說明是一個物件型別
if(hash.get(value)){
return hash.get(value)
}
hash.set(value,obj);
for(let key in value){ // in 會遍歷當前物件上的屬性 和 __proto__指代的屬性
// 補複製 物件的__proto__上的屬性
if(value.hasOwnProperty(key)){
// 如果值還有可能是物件 就繼續複製
obj[key] = deepClone(value[key],hash);
}
}
return obj
// 區分物件和陣列 Object.prototype.toString.call
}
// test
var o = {};
o.x = o;
var o1 = deepClone(o); // 如果這個物件複製過了 就返回那個複製的結果就可以了
console.log(o1);
實現完整的深複製
1. 簡易版及問題
JSON.parse(JSON.stringify());
估計這個api能覆蓋大多數的應用場景,沒錯,談到深複製,我第一個想到的也是它。但是實際上,對於某些嚴格的場景來說,這個方法是有巨大的坑的。問題如下:
- 無法解決
迴圈引用
的問題。舉個例子:
const a = {val:2};
a.target = a;
複製a
會出現系統棧溢位,因為出現了無限遞迴的情況。
- 無法複製一些特殊的物件,諸如
RegExp, Date, Set, Map
等 - 無法複製
函式
(劃重點)。
因此這個api先pass掉,我們重新寫一個深複製,簡易版如下:
const deepClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
} else {
return target;
}
}
現在,我們以剛剛發現的三個問題為導向,一步步來完善、最佳化我們的深複製程式碼。
2. 解決迴圈引用
現在問題如下:
let obj = {val : 100};
obj.target = obj;
deepClone(obj);//報錯: RangeError: Maximum call stack size exceeded
這就是迴圈引用。我們怎麼來解決這個問題呢?
建立一個Map。記錄下已經複製過的物件,如果說已經複製過,那直接返回它行了。
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const deepClone = (target, map = new Map()) => {
if(map.get(target))
return target;
if (isObject(target)) {
map.set(target, true);
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop],map);
}
}
return cloneTarget;
} else {
return target;
}
}
現在來試一試:
const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }
好像是沒有問題了, 複製也完成了。但還是有一個潛在的坑, 就是map 上的 key 和 map 構成了強引用關係,這是相當危險的。我給你解釋一下與之相對的弱引用的概念你就明白了
在計算機程式設計中,弱引用與強引用相對,
被弱引用的物件可以在任何時候被回收,而對於強引用來說,只要這個強引用還在,那麼物件無法被回收。拿上面的例子說,map 和 a一直是強引用的關係, 在程式結束之前,a 所佔的記憶體空間一直不會被釋放。
怎麼解決這個問題?
很簡單,讓 map 的 key 和 map 構成弱引用即可。ES6給我們提供了這樣的資料結構,它的名字叫WeakMap,它是一種特殊的Map, 其中的鍵是弱引用的。其鍵必須是物件,而值可以是任意的
稍微改造一下即可:
const deepClone = (target, map = new WeakMap()) => {
//...
}
3. 複製特殊物件
可繼續遍歷
對於特殊的物件,我們使用以下方式來鑑別:
Object.prototype.toString.call(obj);
梳理一下對於可遍歷物件會有什麼結果:
["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]
以這些不同的字串為依據,我們就可以成功地鑑別這些物件。
const getType = Object.prototype.toString.call(obj);
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const deepClone = (target, map = new Map()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 處理不能遍歷的物件
return;
}else {
// 這波操作相當關鍵,可以保證物件的原型不丟失!
let ctor = target.prototype;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.put(target, true);
if(type === mapTag) {
//處理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key), deepClone(item));
})
}
if(type === setTag) {
//處理Set
target.forEach(item => {
target.add(deepClone(item));
})
}
// 處理陣列和物件
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
}
不可遍歷的物件
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
對於不可遍歷的物件,不同的物件有不同的處理。
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (target) => {
// 待會的重點部分
}
const handleNotTraverse = (target, tag) => {
const Ctor = targe.constructor;
switch(tag) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
4. 複製函式
- 雖然函式也是物件,但是它過於特殊,我們單獨把它拿出來拆解。
- 提到函式,在JS種有兩種函式,一種是普通函式,另一種是箭頭函式。每個普通函式都是
- Function的例項,而箭頭函式不是任何類的例項,每次呼叫都是不一樣的引用。那我們只需要
- 處理普通函式的情況,箭頭函式直接返回它本身就好了。
那麼如何來區分兩者呢?
答案是: 利用原型。箭頭函式是不存在原型的。
const handleFunc = (func) => {
// 箭頭函式直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分別匹配 函式引數 和 函式體
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
5. 完整程式碼展示
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => {
// 箭頭函式直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分別匹配 函式引數 和 函式體
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
const deepClone = (target, map = new WeakMap()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 處理不能遍歷的物件
return handleNotTraverse(target, type);
}else {
// 這波操作相當關鍵,可以保證物件的原型不丟失!
let ctor = target.constructor;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.set(target, true);
if(type === mapTag) {
//處理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map));
})
}
if(type === setTag) {
//處理Set
target.forEach(item => {
cloneTarget.add(deepClone(item, map));
})
}
// 處理陣列和物件
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
}
實現async/await
分析
// generator生成器 生成迭代器iterator
// 預設這樣寫的類陣列是不能被迭代的,缺少迭代方法
let likeArray = {'0': 1, '1': 2, '2': 3, '3': 4, length: 4}
// // 使用迭代器使得可以展開陣列
// // Symbol有很多超程式設計方法,可以改js本身功能
// likeArray[Symbol.iterator] = function () {
// // 迭代器是一個物件 物件中有next方法 每次呼叫next 都需要返回一個物件 {value,done}
// let index = 0
// return {
// next: ()=>{
// // 會自動呼叫這個方法
// console.log('index',index)
// return {
// // this 指向likeArray
// value: this[index],
// done: index++ === this.length
// }
// }
// }
// }
// let arr = [...likeArray]
// console.log('arr', arr)
// 使用生成器返回迭代器
// likeArray[Symbol.iterator] = function *() {
// let index = 0
// while (index != this.length) {
// yield this[index++]
// }
// }
// let arr = [...likeArray]
// console.log('arr', arr)
// 生成器 碰到yield就會暫停
// function *read(params) {
// yield 1;
// yield 2;
// }
// 生成器返回的是迭代器
// let it = read()
// console.log(it.next())
// console.log(it.next())
// console.log(it.next())
// 透過generator來最佳化promise(promise的缺點是不停的鏈式呼叫)
const fs = require('fs')
const path = require('path')
// const co = require('co') // 幫我們執行generator
const promisify = fn=>{
return (...args)=>{
return new Promise((resolve,reject)=>{
fn(...args, (err,data)=>{
if(err) {
reject(err)
}
resolve(data)
})
})
}
}
// promise化
let asyncReadFile = promisify(fs.readFile)
function * read() {
let content1 = yield asyncReadFile(path.join(__dirname,'./data/name.txt'),'utf8')
let content2 = yield asyncReadFile(path.join(__dirname,'./data/' + content1),'utf8')
return content2
}
// 這樣寫太繁瑣 需要藉助co來實現
// let re = read()
// let {value,done} = re.next()
// value.then(data=>{
// // 除了第一次傳參沒有意義外 剩下的傳參都賦予了上一次的返回值
// let {value,done} = re.next(data)
// value.then(d=>{
// let {value,done} = re.next(d)
// console.log(value,done)
// })
// }).catch(err=>{
// re.throw(err) // 手動丟擲錯誤 可以被try catch捕獲
// })
// 實現co原理
function co(it) {// it 迭代器
return new Promise((resolve,reject)=>{
// 非同步迭代 需要根據函式來實現
function next(data) {
// 遞迴得有中止條件
let {value,done} = it.next(data)
if(done) {
resolve(value) // 直接讓promise變成成功 用當前返回的結果
} else {
// Promise.resolve(value).then(data=>{
// next(data)
// }).catch(err=>{
// reject(err)
// })
// 簡寫
Promise.resolve(value).then(next,reject)
}
}
// 首次呼叫
next()
})
}
co(read()).then(d=>{
console.log(d)
}).catch(err=>{
console.log(err,'--')
})
整體看一下結構
function asyncToGenerator(generatorFunc) {
return function() {
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve, reject) => {
function step(key, arg) {
let generatorResult
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
const { value, done } = generatorResult
if (done) {
return resolve(value)
} else {
return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
}
}
step("next")
})
}
}
分析
function asyncToGenerator(generatorFunc) {
// 返回的是一個新的函式
return function() {
// 先呼叫generator函式 生成迭代器
// 對應 var gen = testG()
const gen = generatorFunc.apply(this, arguments)
// 返回一個promise 因為外部是用.then的方式 或者await的方式去使用這個函式的返回值的
// var test = asyncToGenerator(testG)
// test().then(res => console.log(res))
return new Promise((resolve, reject) => {
// 內部定義一個step函式 用來一步一步的跨過yield的阻礙
// key有next和throw兩種取值,分別對應了gen的next和throw方法
// arg引數則是用來把promise resolve出來的值交給下一個yield
function step(key, arg) {
let generatorResult
// 這個方法需要包裹在try catch中
// 如果報錯了 就把promise給reject掉 外部透過.catch可以獲取到錯誤
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 得到的結果是一個 { value, done } 的結構
const { value, done } = generatorResult
if (done) {
// 如果已經完成了 就直接resolve這個promise
// 這個done是在最後一次呼叫next後才會為true
// 以本文的例子來說 此時的結果是 { done: true, value: 'success' }
// 這個value也就是generator函式最後的返回值
return resolve(value)
} else {
// 除了最後結束的時候外,每次呼叫gen.next()
// 其實是返回 { value: Promise, done: false } 的結構,
// 這裡要注意的是Promise.resolve可以接受一個promise為引數
// 並且這個promise引數被resolve的時候,這個then才會被呼叫
return Promise.resolve(
// 這個value對應的是yield後面的promise
value
).then(
// value這個promise被resove的時候,就會執行next
// 並且只要done不是true的時候 就會遞迴的往下解開promise
// 對應gen.next().value.then(value => {
// gen.next(value).value.then(value2 => {
// gen.next()
//
// // 此時done為true了 整個promise被resolve了
// // 最外部的test().then(res => console.log(res))的then就開始執行了
// })
// })
function onResolve(val) {
step("next", val)
},
// 如果promise被reject了 就再次進入step函式
// 不同的是,這次的try catch中呼叫的是gen.throw(err)
// 那麼自然就被catch到 然後把promise給reject掉啦
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}
實現Node的require方法
require 基本原理
require 查詢路徑
require
和module.exports
乾的事情並不複雜,我們先假設有一個全域性物件{}
,初始情況下是空的,當你require
某個檔案時,就將這個檔案拿出來執行,如果這個檔案裡面存在module.exports
,當執行到這行程式碼時將module.exports
的值加入這個物件,鍵為對應的檔名,最終這個物件就長這樣:
{
"a.js": "hello world",
"b.js": function add(){},
"c.js": 2,
"d.js": { num: 2 }
}
當你再次require
某個檔案時,如果這個物件裡面有對應的值,就直接返回給你,如果沒有就重複前面的步驟,執行目標檔案,然後將它的module.exports
加入這個全域性物件,並返回給呼叫者。這個全域性物件其實就是我們經常聽說的快取。所以require
和module.exports
並沒有什麼黑魔法,就只是執行並獲取目標檔案的值,然後加入快取,用的時候拿出來用就行
手寫實現一個require
const path = require('path'); // 路徑操作
const fs = require('fs'); // 檔案讀取
const vm = require('vm'); // 檔案執行
// node模組化的實現
// node中是自帶模組化機制的,每個檔案就是一個單獨的模組,並且它遵循的是CommonJS規範,也就是使用require的方式匯入模組,透過module.export的方式匯出模組。
// node模組的執行機制也很簡單,其實就是在每一個模組外層包裹了一層函式,有了函式的包裹就可以實現程式碼間的作用域隔離
// require載入模組
// require依賴node中的fs模組來載入模組檔案,fs.readFile讀取到的是一個字串。
// 在javascrpt中我們可以透過eval或者new Function的方式來將一個字串轉換成js程式碼來執行。
// eval
// const name = 'poetry';
// const str = 'const a = 123; console.log(name)';
// eval(str); // poetry;
// new Function
// new Function接收的是一個要執行的字串,返回的是一個新的函式,呼叫這個新的函式字串就會執行了。如果這個函式需要傳遞引數,可以在new Function的時候依次傳入引數,最後傳入的是要執行的字串。比如這裡傳入引數b,要執行的字串str
// const b = 3;
// const str = 'let a = 1; return a + b';
// const fun = new Function('b', str);
// console.log(fun(b, str)); // 4
// 可以看到eval和Function例項化都可以用來執行javascript字串,似乎他們都可以來實現require模組載入。不過在node中並沒有選用他們來實現模組化,原因也很簡單因為他們都有一個致命的問題,就是都容易被不屬於他們的變數所影響。
// 如下str字串中並沒有定義a,但是確可以使用上面定義的a變數,這顯然是不對的,在模組化機制中,str字串應該具有自身獨立的執行空間,自身不存在的變數是不可以直接使用的
// const a = 1;
// const str = 'console.log(a)';
// eval(str);
// const func = new Function(str);
// func();
// node存在一個vm虛擬環境的概念,用來執行額外的js檔案,他可以保證javascript執行的獨立性,不會被外部所影響
// vm 內建模組
// 雖然我們在外部定義了hello,但是str是一個獨立的模組,並不在村hello變數,所以會直接報錯。
// 引入vm模組, 不需要安裝,node 自建模組
// const vm = require('vm');
// const hello = 'poetry';
// const str = 'console.log(hello)';
// wm.runInThisContext(str); // 報錯
// 所以node執行javascript模組時可以採用vm來實現。就可以保證模組的獨立性了
// 分析實現步驟
// 1.匯入相關模組,建立一個Require方法。
// 2.抽離透過Module._load方法,用於載入模組。
// 3.Module.resolveFilename 根據相對路徑,轉換成絕對路徑。
// 4.快取模組 Module._cache,同一個模組不要重複載入,提升效能。
// 5.建立模組 id: 儲存的內容是 exports = {}相當於this。
// 6.利用tryModuleLoad(module, filename) 嘗試載入模組。
// 7.Module._extensions使用讀取檔案。
// 8.Module.wrap: 把讀取到的js包裹一個函式。
// 9.將拿到的字串使用runInThisContext執行字串。
// 10.讓字串執行並將this改編成exports
// 定義匯入類,引數為模組路徑
function Require(modulePath) {
// 獲取當前要載入的絕對路徑
let absPathname = path.resolve(__dirname, modulePath);
// 自動給模組新增字尾名,實現省略字尾名載入模組,其實也就是如果檔案沒有字尾名的時候遍歷一下所有的字尾名看一下檔案是否存在
// 獲取所有字尾名
const extNames = Object.keys(Module._extensions);
let index = 0;
// 儲存原始檔案路徑
const oldPath = absPathname;
function findExt(absPathname) {
if (index === extNames.length) {
throw new Error('檔案不存在');
}
try {
fs.accessSync(absPathname);
return absPathname;
} catch(e) {
const ext = extNames[index++];
findExt(oldPath + ext);
}
}
// 遞迴追加字尾名,判斷檔案是否存在
absPathname = findExt(absPathname);
// 從快取中讀取,如果存在,直接返回結果
if (Module._cache[absPathname]) {
return Module._cache[absPathname].exports;
}
// 建立模組,新建Module例項
const module = new Module(absPathname);
// 新增快取
Module._cache[absPathname] = module;
// 載入當前模組
tryModuleLoad(module);
// 返回exports物件
return module.exports;
}
// Module的實現很簡單,就是給模組建立一個exports物件,tryModuleLoad執行的時候將內容加入到exports中,id就是模組的絕對路徑
// 定義模組, 新增檔案id標識和exports屬性
function Module(id) {
this.id = id;
// 讀取到的檔案內容會放在exports中
this.exports = {};
}
Module._cache = {};
// 我們給Module掛載靜態屬性wrapper,裡面定義一下這個函式的字串,wrapper是一個陣列,陣列的第一個元素就是函式的引數部分,其中有exports,module. Require,__dirname, __filename, 都是我們模組中常用的全域性變數。注意這裡傳入的Require引數是我們自己定義的Require
// 第二個引數就是函式的結束部分。兩部分都是字串,使用的時候我們將他們包裹在模組的字串外部就可以了
Module.wrapper = [
"(function(exports, module, Require, __dirname, __filename) {",
"})"
]
// _extensions用於針對不同的模組副檔名使用不同的載入方式,比如JSON和javascript載入方式肯定是不同的。JSON使用JSON.parse來執行。
// javascript使用vm.runInThisContext來執行,可以看到fs.readFileSync傳入的是module.id也就是我們Module定義時候id儲存的是模組的絕對路徑,讀取到的content是一個字串,我們使用Module.wrapper來包裹一下就相當於在這個模組外部又包裹了一個函式,也就實現了私有作用域。
// 使用call來執行fn函式,第一個引數改變執行的this我們傳入module.exports,後面的引數就是函式外面包裹引數exports, module, Require, __dirname, __filename
Module._extensions = {
'.js'(module) {
const content = fs.readFileSync(module.id, 'utf8');
const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
const fn = vm.runInThisContext(fnStr);
fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
},
'.json'(module) {
const json = fs.readFileSync(module.id, 'utf8');
module.exports = JSON.parse(json); // 把檔案的結果放在exports屬性上
}
}
// tryModuleLoad函式接收的是模組物件,透過path.extname來獲取模組的字尾名,然後使用Module._extensions來載入模組
// 定義模組載入方法
function tryModuleLoad(module) {
// 獲取副檔名
const extension = path.extname(module.id);
// 透過字尾載入當前模組
Module._extensions[extension](module);
}
// 至此Require載入機制我們基本就寫完了,我們來重新看一下。Require載入模組的時候傳入模組名稱,在Require方法中使用path.resolve(__dirname, modulePath)獲取到檔案的絕對路徑。然後透過new Module例項化的方式建立module物件,將模組的絕對路徑儲存在module的id屬性中,在module中建立exports屬性為一個json物件
// 使用tryModuleLoad方法去載入模組,tryModuleLoad中使用path.extname獲取到檔案的副檔名,然後根據副檔名來執行對應的模組載入機制
// 最終將載入到的模組掛載module.exports中。tryModuleLoad執行完畢之後module.exports已經存在了,直接返回就可以了
// 給模組新增快取
// 新增快取也比較簡單,就是檔案載入的時候將檔案放入快取中,再去載入模組時先看快取中是否存在,如果存在直接使用,如果不存在再去重新,載入之後再放入快取
// 測試
let json = Require('./test.json');
let test2 = Require('./test2.js');
console.log(json);
console.log(test2);
實現JSONP方法
利用<script>
標籤不受跨域限制的特點,缺點是隻能支援get
請求
- 建立
script
標籤 - 設定
script
標籤的src
屬性,以問號傳遞引數,設定好回撥函式callback
名稱 - 插入到
html
文字中 - 呼叫回撥函式,
res
引數就是獲取的資料
function jsonp({url,params,callback}) {
return new Promise((resolve,reject)=>{
let script = document.createElement('script')
window[callback] = function (data) {
resolve(data)
document.body.removeChild(script)
}
var arr = []
for(var key in params) {
arr.push(`${key}=${params[key]}`)
}
script.type = 'text/javascript'
script.src = `${url}?callback=${callback}&${arr.join('&')}`
document.body.appendChild(script)
})
}
// 測試用例
jsonp({
url: 'http://suggest.taobao.com/sug',
callback: 'getData',
params: {
q: 'iphone手機',
code: 'utf-8'
},
}).then(data=>{console.log(data)})
- 設定
CORS: Access-Control-Allow-Origin:*
postMessage
實現ES6的extends
function B(name){
this.name = name;
};
function A(name,age){
//1.將A的原型指向B
Object.setPrototypeOf(A,B);
//2.用A的例項作為this呼叫B,得到繼承B之後的例項,這一步相當於呼叫super
Object.getPrototypeOf(A).call(this, name)
//3.將A原有的屬性新增到新例項上
this.age = age;
//4.返回新例項物件
return this;
};
var a = new A('poetry',22);
console.log(a);
實現Array.of方法
Array.of()
方法用於將一組值,轉換為陣列
- 這個方法的主要目的,是彌補陣列建構函式
Array()
的不足。因為引數個數的不同,會導致Array()
的行為有差異。 Array.of()
基本上可以用來替代Array()
或new Array()
,並且不存在由於引數不同而導致的過載。它的行為非常統一
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
實現
function ArrayOf(){
return [].slice.call(arguments);
}
reduce用法彙總
語法
array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
/*
total: 必需。初始值, 或者計算結束後的返回值。
currentValue: 必需。當前元素。
currentIndex: 可選。當前元素的索引;
arr: 可選。當前元素所屬的陣列物件。
initialValue: 可選。傳遞給函式的初始值,相當於total的初始值。
*/
reduceRight()
該方法用法與reduce()
其實是相同的,只是遍歷的順序相反,它是從陣列的最後一項開始,向前遍歷到第一項
1. 陣列求和
const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num);
// 設定初始值求和
const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num, 10); // 以10為初始值求和
// 物件陣列求和
var result = [
{ subject: 'math', score: 88 },
{ subject: 'chinese', score: 95 },
{ subject: 'english', score: 80 }
];
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, 0);
const sum = result.reduce((accumulator, cur) => accumulator + cur.score, -10); // 總分扣除10分
2. 陣列最大值
const a = [23,123,342,12];
const max = a.reduce((pre,next)=>pre>cur?pre:cur,0); // 342
3. 陣列轉物件
var streams = [{name: '技術', id: 1}, {name: '設計', id: 2}];
var obj = streams.reduce((accumulator, cur) => {accumulator[cur.id] = cur; return accumulator;}, {});
4. 扁平一個二維陣列
var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((x, y) => x.concat(y), []);
5. 陣列去重
實現的基本原理如下:
① 初始化一個空陣列
② 將需要去重處理的陣列中的第1項在初始化陣列中查詢,如果找不到(空陣列中肯定找不到),就將該項新增到初始化陣列中
③ 將需要去重處理的陣列中的第2項在初始化陣列中查詢,如果找不到,就將該項繼續新增到初始化陣列中
④ ……
⑤ 將需要去重處理的陣列中的第n項在初始化陣列中查詢,如果找不到,就將該項繼續新增到初始化陣列中
⑥ 將這個初始化陣列返回
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
6. 物件陣列去重
const dedup = (data, getKey = () => { }) => {
const dateMap = data.reduce((pre, cur) => {
const key = getKey(cur)
if (!pre[key]) {
pre[key] = cur
}
return pre
}, {})
return Object.values(dateMap)
}
7. 求字串中字母出現的次數
const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha';
const res = str.split('').reduce((pre,next)=>{
pre[next] ? pre[next]++ : pre[next] = 1
return pre
},{})
// 結果
-: 1
a: 8
c: 1
d: 4
e: 1
f: 4
g: 1
h: 2
i: 2
j: 4
k: 1
l: 3
m: 2
n: 1
q: 5
r: 1
s: 6
u: 1
w: 2
x: 1
z: 1
8. compose函式
redux compose
原始碼實現
function compose(...funs) {
if (funs.length === 0) {
return arg => arg;
}
if (funs.length === 1) {
return funs[0];
}
return funs.reduce((a, b) => (...arg) => a(b(...arg)))
}
版本號排序的方法
題目描述:有一組版本號如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']
。現在需要對其進行排序,排序的結果為 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split(".");
const arr2 = b.split(".");
while (true) {
const s1 = arr1[i];
const s2 = arr2[i];
i++;
if (s1 === undefined || s2 === undefined) {
return arr2.length - arr1.length;
}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr);
實現map方法
- 回撥函式的引數有哪些,返回值如何處理
- 不修改原來的陣列
Array.prototype.myMap = function(callback, context){
// 轉換類陣列
var arr = Array.prototype.slice.call(this),//由於是ES5所以就不用...展開符了
mappedArr = [],
i = 0;
for (; i < arr.length; i++ ){
// 把當前值、索引、當前陣列返回去。呼叫的時候傳到函式引數中 [1,2,3,4].map((curr,index,arr))
mappedArr.push(callback.call(context, arr[i], i, this));
}
return mappedArr;
}
實現一個JSON.parse
JSON.parse(text[, reviver])
用來解析JSON字串,構造由字串描述的JavaScript值或物件。提供可選的reviver函式用以在返回之前對所得到的物件執行變換(操作)
第一種:直接呼叫 eval
function jsonParse(opt) {
return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
避免在不必要的情況下使用eval
,eval()
是一個危險的函式,他執行的程式碼擁有著執行者的權利。如果你用eval()
執行的字串程式碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴充套件程式的許可權下,在使用者計算機上執行惡意程式碼。它會執行JS程式碼,有XSS漏洞。
如果你只想記這個方法,就得對引數json做校驗。
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
rx_one.test(
json
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
var obj = eval("(" +json + ")");
}
第二種:Function
核心:Function與eval有相同的字串引數特性
var func = new Function(arg1, arg2, ..., functionBody);
在轉換JSON的實際應用中,只需要這麼做
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
eval
與Function
都有著動態編譯js程式碼的作用,但是在實際的程式設計中並不推薦使用
轉化為駝峰命名
var s1 = "get-element-by-id"
// 轉化為 getElementById
var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
原生實現
function ajax() {
let xhr = new XMLHttpRequest() //例項化,以呼叫方法
xhr.open('get', 'https://www.google.com') //引數2,url。引數三:非同步
xhr.onreadystatechange = () => { //每當 readyState 屬性改變時,就會呼叫該函式。
if (xhr.readyState === 4) { //XMLHttpRequest 代理當前所處狀態。
if (xhr.status >= 200 && xhr.status < 300) { //200-300請求成功
let string = request.responseText
//JSON.parse() 方法用來解析JSON字串,構造由字串描述的JavaScript值或物件
let object = JSON.parse(string)
}
}
}
request.send() //用於實際發出 HTTP 請求。不帶引數為GET請求
}
小孩報數問題
有30個小孩兒,編號從1-30,圍成一圈依此報數,1、2、3 數到 3 的小孩兒退出這個圈, 然後下一個小孩 重新報數 1、2、3,問最後剩下的那個小孩兒的編號是多少?
function childNum(num, count){
let allplayer = [];
for(let i = 0; i < num; i++){
allplayer[i] = i + 1;
}
let exitCount = 0; // 離開人數
let counter = 0; // 記錄報數
let curIndex = 0; // 當前下標
while(exitCount < num - 1){
if(allplayer[curIndex] !== 0) counter++;
if(counter == count){
allplayer[curIndex] = 0;
counter = 0;
exitCount++;
}
curIndex++;
if(curIndex == num){
curIndex = 0
};
}
for(i = 0; i < num; i++){
if(allplayer[i] !== 0){
return allplayer[i]
}
}
}
childNum(30, 3)