前端面試之手寫程式碼

八叉樹發表於2019-05-19

本系列會從面試的角度出發圍繞JavaScriptNode.js(npm包)以及框架三個方面來對常見的模擬實現進行總結,具體原始碼放在github專案上,長期更新和維護

前端面試之手寫程式碼

陣列去重

(一維)陣列去重最原始的方法就是使用雙層迴圈,分別迴圈原始陣列和新建陣列;或者我們可以使用indexOf來簡化內層的迴圈;或者可以將原始陣列排序完再來去重,這樣會減少一個迴圈,只需要比較前後兩個數即可;當然我們可以使用ES5,ES6的方法來簡化去重的寫法,比如我們可以使用filter來簡化內層迴圈,或者使用SetMap、擴充套件運算子這些用起來更簡單的方法,但是效率上應該不會比原始方法好。二維陣列的去重可以在上面方法的基礎上再判斷元素是不是陣列,如果是的話,就進行遞迴處理。

雙層迴圈

var array = [1, 1, '1', '1'];

function unique(array) {
    var res = [];
    for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
        for (var j = 0, resLen = res.length; j < resLen; j++ ) {
            if (array[i] === res[j]) {
                break;
            }
        }
        if (j === resLen) {
            res.push(array[i])
        }
    }
    return res;
}

console.log(unique(array)); // [1, "1"]
複製程式碼

利用indexOf

var array = [1, 1, '1'];

function unique(array) {
    var res = [];
    for (var i = 0, len = array.length; i < len; i++) {
        var current = array[i];
        if (res.indexOf(current) === -1) {
            res.push(current)
        }
    }
    return res;
}

console.log(unique(array));
複製程式碼

排序後去重

var array = [1, 1, '1'];

function unique(array) {
    var res = [];
    var sortedArray = array.concat().sort();
    var seen;
    for (var i = 0, len = sortedArray.length; i < len; i++) {
        // 如果是第一個元素或者相鄰的元素不相同
        if (!i || seen !== sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i];
    }
    return res;
}

console.log(unique(array));
複製程式碼

filter

filter可以用來簡化外層迴圈

使用indexOf:

var array = [1, 2, 1, 1, '1'];

function unique(array) {
    var res = array.filter(function(item, index, array){
        return array.indexOf(item) === index;
    })
    return res;
}

console.log(unique(array));
複製程式碼

排序去重:

var array = [1, 2, 1, 1, '1'];

function unique(array) {
    return array.concat().sort().filter(function(item, index, array){
        return !index || item !== array[index - 1]
    })
}

console.log(unique(array));
複製程式碼

ES6方法

Set:

var array = [1, 2, 1, 1, '1'];

function unique(array) {
   return Array.from(new Set(array));
}

console.log(unique(array)); // [1, 2, "1"]
複製程式碼

再簡化下

function unique(array) {
    return [...new Set(array)];
}

//或者
var unique = (a) => [...new Set(a)]
複製程式碼

Map:

function unique (arr) {
    const seen = new Map()
    return arr.filter((a) => !seen.has(a) && seen.set(a, 1))
}
複製程式碼

型別判斷

型別判斷需要注意以下幾點

  • typeof對六個基本資料型別UndefinedNullBooleanNumberStringObject(大寫)返回的結果是

    undefinedobjectbooleannumberstringobject(小寫),可以看到NullObject 型別都返回了 object 字串;typeof卻能檢測出函式型別;綜上,typeof能檢測出六種型別,但是不能檢測出null型別和Object下細分的型別,如ArrayFunctionDateRegExp,Error

  • Object.prototype.toString的作用非常強大,它能檢測出基本資料型別以及Object下的細分型別,甚至像 Math,JSON,arguments它都能檢測出它們的具體型別,它返回結果形式例如[object Number](注意最後的資料型別是大寫).所以,Object.prototype.toString基本上能檢測出所有的型別了,只不過有時需要考慮到相容性低版本瀏覽器的問題。

通用API

    
// 該型別判斷函式可以判斷六種基本資料型別以及Boolean Number String Function Array Date RegExp Object Error,
// 其他型別因為遇到型別判斷的情況較少所以都會返回object,不在進行詳細的判斷
//  比如ES6新增的Symbol,Map,Set等型別
var classtype = {};


"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item) {
    classtype["[object " + item + "]"] = item.toLowerCase();
})


function type(obj) {
    // 解決IE6中null和undefined會被Object.prototype.toString識別成[object Object]
    if (obj == null) {
        return obj + "";
    }

    //如果是typeof後型別為object下的細分型別(Array,Function,Date,RegExp,Error)或者是Object型別,則要利用Object.prototype.toString
    //由於ES6新增的Symbol,Map,Set等型別不在classtype列表中,所以使用type函式,返回的結果會是object
    return typeof obj === "object" || typeof obj === "function" ?
        classtype[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}
複製程式碼

判斷空物件

判斷是否有屬性,for迴圈一旦執行,就說明有屬性,此時返回false

function isEmptyObject( obj ) {
        var name;
        for ( name in obj ) {
            return false;
        }
        return true;
}

console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true
複製程式碼

我們可以看出isEmptyObject實際上判斷的並不僅僅是空物件。但是既然jQuery是這樣寫,可能是因為考慮到實際開發中 isEmptyObject用來判斷 {} 和 {a: 1} 是足夠的吧。如果真的是隻判斷 {},完全可以結合上篇寫的 type函式篩選掉不適合的情況。

判斷Window物件

Window物件有一個window屬性指向自身,可以利用這個特性來判斷是否是Window物件

function isWindow( obj ) {
    return obj != null && obj === obj.window;
}
複製程式碼

判斷陣列

isArray是陣列型別內建的資料型別判斷函式,但是會有相容性問題,一個polyfill如下

isArray = Array.isArray || function(array){
  return Object.prototype.toString.call(array) === '[object Array]';
}
複製程式碼

判斷類陣列

jquery實現的isArrayLike,陣列和類陣列都會返回true。所如果isArrayLike返回true,至少要滿足三個條件之一:

  1. 是陣列

  2. 長度為 0 比如下面情況,如果我們去掉length === 0 這個判斷,就會列印 false,然而我們都知道 arguments 是一個類陣列物件,這裡是應該返回 true

    function a(){
        console.log(isArrayLike(arguments))
    }
    a();
    複製程式碼
  3. lengths 屬性是大於 0 的數字型別,並且obj[length - 1]必須存在(考慮到arr = [,,3]的情況)

function isArrayLike(obj) {

    // obj 必須有 length屬性
    var length = !!obj && "length" in obj && obj.length;
    var typeRes = type(obj);

    // 排除掉函式和 Window 物件
    if (typeRes === "function" || isWindow(obj)) {
        return false;
    }

    return typeRes === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}
複製程式碼

判斷NaN

判斷一個數是不是NaN不能單純地使用 === 這樣來判斷, 因為NaN不與任何數相等, 包括自身,注意在ES6isNaN中只有值為數字型別使用NaN才會返回true

isNaN: function(value){
  return isNumber(value) && isNaN(value);
}
複製程式碼

判斷DOM元素

利用DOM物件特有的nodeType屬性(

isElement: function(obj){
  return !!(obj && obj.nodeType === 1);
    // 兩次感嘆號將值轉化為布林值
}
複製程式碼

判斷arguments物件

低版本的瀏覽器中argument物件通過Object.prototype.toString判斷後返回的是[object Object],所以需要相容

isArguments: function(obj){
  return Object.prototype.toString.call(obj) === '[object Arguments]' || (obj != null && Object.hasOwnProperty.call(obj, 'callee'));
}
複製程式碼

深淺拷貝

如果是陣列,實現淺拷貝,比可以sliceconcat返回一個新陣列的特性來實現;實現深拷貝,可以利用JSON.parseJSON.stringify來實現,但是有一個問題,不能拷貝函式(此時拷貝後返回的陣列為null)。上面的方法都屬於技巧,下面考慮怎麼實現一個物件或者陣列的深淺拷貝

淺拷貝

思路很簡單,遍歷物件,然後把屬性和屬性值都放在一個新的物件就OK了

var shallowCopy = function(obj) {
    // 只拷貝物件
    if (typeof obj !== 'object') return;
    // 根據obj的型別判斷是新建一個陣列還是物件
    var newObj = obj instanceof Array ? [] : {};
    // 遍歷obj,並且判斷是obj的屬性才拷貝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
複製程式碼

深拷貝

思路也很簡單,就是在拷貝的時候判斷一下屬性值的型別,如果是物件,就遞迴呼叫深淺拷貝函式就ok了

var deepCopy = function(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}
複製程式碼

扁平化

遞迴

迴圈陣列元素,如果還是一個陣列,就遞迴呼叫該方法

// 方法 1
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        }
        else {
            result.push(arr[i])
        }
    }
    return result;
}


console.log(flatten(arr))
複製程式碼

toString()

如果陣列的元素都是數字,可以使用該方法

// 方法2
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.toString().split(',').map(function(item){
        return +item // +會使字串發生型別轉換
    })
}

console.log(flatten(arr))
複製程式碼

reduce()

// 方法3
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

console.log(flatten(arr))
複製程式碼

...

// 扁平化一維陣列
var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]

// 可以扁平化多維陣列
var arr = [1, [2, [3, 4]]];

function flatten(arr) {

    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }

    return arr;
}

console.log(flatten(arr))
複製程式碼

柯里化

通用版

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);
        }
    }
}

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 debounce(fn, wait) {
    var timeout = null;
    return function() {
        if(timeout !== null) 
        {
                clearTimeout(timeout);
        }
        timeout = setTimeout(fn, wait);
    }
}
// 處理函式
function handle() {
    console.log(Math.random()); 
}
// 滾動事件
window.addEventListener('scroll', debounce(handle, 1000));
複製程式碼

節流

利用時間戳實現

   var throttle = function(func, delay) {
            var prev = 0;
            return function() {
                var context = this;
                var args = arguments;
                var now = Date.now();
                if (now - prev >= delay) {
                    func.apply(context, args);
                    prev = Date.now();
                }
            }
        }
        function handle() {
            console.log(Math.random());
        }
        window.addEventListener('scroll', throttle(handle, 1000));
複製程式碼

利用定時器實現

   var throttle = function(func, delay) {
            var timer = null;
            return function() {
                var context = this;
                var args = arguments;
                if (!timer) {
                    timer = setTimeout(function() {
                        func.apply(context, args);
                        timer = null;
                    }, delay);
                }
            }
        }
        function handle() {
            console.log(Math.random());
        }
        window.addEventListener('scroll', throttle(handle, 1000));
複製程式碼

利用時間戳+定時器

節流中用時間戳或定時器都是可以的。更精確地,可以用時間戳+定時器,當第一次觸發事件時馬上執行事件處理函式,最後一次觸發事件後也還會執行一次事件處理函式。

var throttle = function(func, delay) {
     var timer = null;
     var startTime = 0;
     return function() {
             var curTime = Date.now();
             var remaining = delay - (curTime - startTime);
             var context = this;
             var args = arguments;
             clearTimeout(timer);
              if (remaining <= 0) {
                    func.apply(context, args);
                    startTime = Date.now();
              } else {
                    timer = setTimeout(func, remaining);
              }
      }
}
function handle() {
      console.log(Math.random());
}
 window.addEventListener('scroll', throttle(handle, 1000));
複製程式碼

模擬new

  • new產生的例項可以訪問Constructor裡的屬性,也可以訪問到Constructor.prototype中的屬性,前者可以通過apply來實現,後者可以通過將例項的proto屬性指向建構函式的prototype來實現
  • 我們還需要判斷返回的值是不是一個物件,如果是一個物件,我們就返回這個物件,如果沒有,我們該返回什麼就返回什麼
function New(){
    var obj=new Object();
    //取出第一個引數,就是我們要傳入的建構函式;此外因為shift會修改原陣列,所以arguments會被去除第一個引數
    Constructor=[].shift.call(arguments);
    //將obj的原型指向建構函式,這樣obj就可以訪問到建構函式原型中的屬性
    obj._proto_=Constructor.prototype;
    //使用apply改變建構函式this的指向到新建的物件,這樣obj就可以訪問到建構函式中的屬性
    var ret=Constructor.apply(obj,arguments);
    //要返回obj
    return typeof ret === 'object' ? ret:obj;
}
複製程式碼
function Otaku(name,age){
	this.name=name;
	this.age=age;
	this.habit='Games'
}

Otaku.prototype.sayYourName=function(){
    console.log("I am" + this.name);
}

var person=objectFactory(Otaku,'Kevin','18')

console.log(person.name)//Kevin
console.log(person.habit)//Games
console.log(person.strength)//60
複製程式碼

模擬call

  • call()方法在使用一個指定的this值和若干個指定的引數值的前提下呼叫某個函式或方法
  • 模擬的步驟是:將函式設為物件的屬性—>執行該函式—>刪除該函式
  • this引數可以傳null,當為null的時候,視為指向window
  • 函式是可以有返回值的

簡單版

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
}
foo.bar() // 1
複製程式碼

完善版

Function.prototype.call2 = function(context) {
    var context=context||window
    context.fn = this;
    let args = [...arguments].slice(1);
    let result = context.fn(...args);
    delete context.fn;
    return result;
}
let foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
//表示bar函式的執行環境是foo,即bar函式裡面的this代表foo,this.value相當於foo.value,然後給bar函式傳遞兩個引數
bar.call2(foo, 'black', '18') // black 18 1
複製程式碼

模擬apply

  • apply()的實現和call()類似,只是引數形式不同
Function.prototype.apply2 = function(context = window) {
    context.fn = this
    let result;
    // 判斷是否有第二個引數
    if(arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return result
}
複製程式碼

模擬bind

Function.prototype.bind2=function(context){
    var self=this
    var args=Array.prototype.slice.call(arguments,1);
    
    var fNOP=function(){};
    var fBound=function(){
        var bindArgs=Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindAt))
    }
}
複製程式碼

模擬instanceof

function instanceOf(left,right) {

    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto === null) return false
        if(proto === prototype) return true
        proto = proto.__proto__;
    }
}
複製程式碼

模擬JSON.stringify

JSON.stringify(value[, replacer [, space]])

  • Boolean | Number| String 型別會自動轉換成對應的原始值。

  • undefined、任意函式以及symbol,會被忽略(出現在非陣列物件的屬性值中時),或者被轉換成 null(出現在陣列中時)。

  • 不可列舉的屬性會被忽略

  • 如果一個物件的屬性值通過某種間接的方式指回該物件本身,即迴圈引用,屬性也會被忽略。

function jsonStringify(obj) {
    let type = typeof obj;
    if (type !== "object") {
        if (/string|undefined|function/.test(type)) {
            obj = '"' + obj + '"';
        }
        return String(obj);
    } else {
        let json = []
        let arr = Array.isArray(obj)
        for (let k in obj) {
            let v = obj[k];
            let type = typeof v;
            if (/string|undefined|function/.test(type)) {
                v = '"' + v + '"';
            } else if (type === "object") {
                v = jsonStringify(v);
            }
            json.push((arr ? "" : '"' + k + '":') + String(v));
        }
        return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
    }
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
複製程式碼

模擬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()執行的字串程式碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴充套件程式的許可權下,在使用者計算機上執行惡意程式碼

利用new Function()

Functioneval有相同的字串引數特性,evalFunction 都有著動態編譯js程式碼的作用,但是在實際的程式設計中並不推薦使用。

var func = new Function(arg1, arg2, ..., functionBody)

var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
複製程式碼

建立物件

建立自定義物件最簡單的方式就是建立一個Object的例項,然後再為它新增屬性和方法,早期的開發人員經常使用這種模式來建立物件,後來物件字面量的方法成了建立物件的首選模式。雖然object建構函式或者物件字面量的方法都可以用來建立物件,但是這些方法使用同一個介面建立很多物件,會產生大量的重複程式碼。為了解決這個問題,人們開始使用各種模式來建立物件,在這些模式中,一般推薦使用四種方式,包括建構函式模式原型模式建構函式和原型組合模式動態原型模式,其他的方式,包括工廠模式寄生建構函式模式穩妥建構函式模式平時使用的較少。而這些方式中,用的最多最推薦的應是組合模式和動態原型模式

建構函式和原型組合模式

優點:

  1. 解決了原型模式對於引用物件的缺點
  2. 解決了原型模式沒有辦法傳遞引數的缺點
  3. 解決了建構函式模式不能共享方法的缺點
function Person(name) {
  this.name = name
  this.friends = ['lilei']
}
Person.prototype.say = function() {
  console.log(this.name)
}

var person1 = new Person('hanmeimei')
person1.say() //hanmeimei
複製程式碼

動態原型模式

優點:

  1. 可以在初次呼叫建構函式的時候就完成原型物件的修改
  2. 修改能體現在所有的例項中
function Person(name) {
  this.name = name
    // 檢測say 是不是一個函式
    // 實際上只在當前第一次時候沒有建立的時候在原型上新增sayName方法
    //因為建構函式執行時,裡面的程式碼都會執行一遍,而原型有一個就行,不用每次都重複,所以僅在第一執行時生成一個原型,後面執行就不必在生成,所以就不會執行if包裹的函式,
//其次為什麼不能再使用字面量的寫法,我們都知道,使用建構函式其實是把new出來的物件作用域繫結在建構函式上,而字面量的寫法,會重新生成一個新物件,就切斷了兩者的聯絡!
  if(typeof this.say != 'function') {
    Person.prototype.say = function(
    alert(this.name)
  }
}
複製程式碼

繼承

原型鏈繼承不僅會帶來引用缺陷,而且我們也無法為不同的例項初始化繼承來的屬性;建構函式繼承方式可以避免類式繼承的缺陷,但是我們無法獲取到父類的共有方法,也就是通過原型prototype繫結的方法;組合繼承解決了上面兩種方式的存在的問題,但是它呼叫了兩次父類的建構函式;寄生組合式繼承強化的部分就是在組合繼承的基礎上減少一次多餘的呼叫父類的建構函式。推薦使用組合繼承方式、寄生組合方式和ES6 extends繼承,建議在實際生產中直接使用ES6的繼承方式。

組合繼承

// 宣告父類   
function Animal(color) {    
  this.name = 'animal';    
  this.type = ['pig','cat'];    
  this.color = color;   
}     

// 新增共有方法  
Animal.prototype.greet = function(sound) {    
  console.log(sound);   
}     

// 宣告子類   
function Dog(color) { 
  // 建構函式繼承    
  Animal.apply(this, arguments);   
}   

// 類式繼承
Dog.prototype = new Animal();   

var dog = new Dog('白色');   
var dog2 = new Dog('黑色');     

dog.type.push('dog');   
console.log(dog.color); // "白色"
console.log(dog.type);  // ["pig", "cat", "dog"]

console.log(dog2.type); // ["pig", "cat"]
console.log(dog2.color);  // "黑色"
dog.greet('汪汪');  // "汪汪"
複製程式碼

注:組合繼承利用上面的方式會使得兩次呼叫父類建構函式,其實我們可以通過Dog.prototype = Animal.prototype; Dog.prototype.constructor = Dog來優化組合繼承,當然終極優化方式就是下面的寄生組合方式。想要了解組合繼承具體優化的可以參考 深入理解JavaScript原型鏈與繼承

寄生組合繼承

function Animal(color) {
  this.color = color;
  this.name = 'animal';
  this.type = ['pig', 'cat'];
}

Animal.prototype.greet = function(sound) {
  console.log(sound);
}


function Dog(color) {
  Animal.apply(this, arguments);
  this.name = 'dog';
}
/* 注意下面兩行 */
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.getName = function() {
  console.log(this.name);
}


var dog = new Dog('白色');   
var dog2 = new Dog('黑色');     

dog.type.push('dog');   
console.log(dog.color);   // "白色"
console.log(dog.type);   // ["pig", "cat", "dog"]

console.log(dog2.type);  // ["pig", "cat"]
console.log(dog2.color);  // "黑色"
dog.greet('汪汪');  //  "汪汪"
複製程式碼

Object.create()的淺拷貝的作用類式下面的函式:

function create(obj) {
  function F() {};
  F.prototype = obj;
  return new F();
}
複製程式碼

需注意一點,由於對Animal的原型進行了拷貝後賦給Dog.prototype,因此Dog.prototype上的constructor屬性也被重寫了,所以我們要修復這一個問題:

Dog.prototype.constructor = Dog;
複製程式碼

extends繼承

class Animal {   
  constructor(color) {   
    this.color = color;   
  }   
  greet(sound) {   
    console.log(sound);   
  }  
}   

class Dog extends Animal {   
  constructor(color) {   
    super(color);   
    this.color = color;   
  }  
}   

let dog = new Dog('黑色');  
dog.greet('汪汪');  // "汪汪"
console.log(dog.color); // "黑色"
複製程式碼

模擬ajax

  • ajax請求過程:建立XMLHttpRequest物件、連線伺服器、傳送請求、接收響應資料
  • 建立後的XMLHttpRequest物件例項擁有很多方法和屬性
    • open方法類似於初始化,並不會發起真正的請求;send方傳送請求,並接受一個可選引數
    • 當請求方式為post時,可以將請求體的引數傳入;當請求方式為get時,可以不傳或傳入null;
    • 不管是getpost,引數都需要通過encodeURIComponent編碼後拼接

通用版

//對請求data進行格式化處理
function formateData(data) {
    let arr = [];
    for (let key in data) {
        //避免有&,=,?字元,對這些字元進行序列化
        arr.push(encodeURIComponent(key) + '=' + data[key])
    }
    return arr.join('&');
}

function ajax(params) {
    //先對params進行處理,防止為空
    params = params || {};
    params.data = params.data || {};

    //普通GET,POST請求
    params.type = (params.type || 'GET').toUpperCase();
    params.data = formateData(params.data);
    //如果是在ie6瀏覽器,那麼XMLHttoRequest是不存在的,應該呼叫ActiveXObject;
    let xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    if (params.type === 'GET') {
        xhr.open(params.type, params.url + '?' + params.data, true);
        xhr.send();
    } else {
        xhr.open(params.type, params.url, true);
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
        xhr.send(params.data);
    }
    // 這裡有兩種寫法,第一種寫法:當xhr.readyState===4的時候,會觸發onload事件,直接通過onload事件 進行回撥函式處理
    xhr.onload = function () {
        if (xhr.status === 200 || xhr.status === 304 || xhr.status === 206) {
            var res;

            if (params.success && params.success instanceof Function) {
                res = JSON.parse(xhr.responseText);
                params.success.call(xhr, res);
            }
        } else {
            if (params.error && params.error instanceof Function) {
                res = xhr.responseText;
                params.error.call(xhr, res);
            }
        }

    }
    //第二種寫法,當xhr.readyState===4時候,說明請求成功返回了,進行成功回撥
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            // 進行onload裡面的處理函式
        }
    }

}
複製程式碼

promise版

// 使用promise實現一個簡單的ajax

/**
 * 首先,可能會使用到的xhr方法或者說屬性
 * onloadstart // 開始傳送時觸發
 * onloadend   // 傳送結束時觸發,無論成功不成功
 * onload      // 得到響應
 * onprogress  // 從伺服器上下載資料,每50ms觸發一次
 * onuploadprogress // 上傳到伺服器的回撥
 * onerror     // 請求錯誤時觸發
 * onabort     // 呼叫abort時候觸發
 * status      // 返回狀態碼
 * setRequestHeader // 設定請求頭
 * responseType // 請求傳入的資料
 */

// 預設的ajax引數
let ajaxDefaultOptions = {
  url: '#', // 請求地址,預設為空
  method: 'GET', // 請求方式,預設為GET請求
  async: true, // 請求同步還是非同步,預設非同步
  timeout: 0, // 請求的超時時間
  dataType: 'text', // 請求的資料格式,預設為text
  data: null, // 請求的引數,預設為空
  headers: {}, // 請求頭,預設為空
  onprogress: function () {}, // 從伺服器下載資料的回撥
  onuploadprogress: function () {}, // 處理上傳檔案到伺服器的回撥
  xhr: null // 允許函式外部建立xhr傳入,但是必須不能是使用過的
};

function _ajax(paramOptions) {
  let options = {};
  for (const key in ajaxDefaultOptions) {
    options[key] = ajaxDefaultOptions[key];
  }
  // 如果傳入的是否非同步與預設值相同,就使用預設值,否則使用傳入的引數
  options.async = paramOptions.async === ajaxDefaultOptions.async ? ajaxDefaultOptions.async : paramOptions.async;
  // 判斷傳入的method是否為GET或者POST,否則傳入GET 或者可將判斷寫在promise內部,reject出去
  options.method = paramOptions.method ? ("GET" || "POST") : "GET";
  // 如果外部傳入xhr,否則建立一個
  let xhr = options.xhr || new XMLHttpRequest();
  // return promise物件
  return new Promise(function (resolve, reject) {
    xhr.open(options.method, options.url, options.async);
    xhr.timeout = options.timeout;
    // 設定請求頭
    for (const key in options.headers) {
      xhr.setRequestHeader(key, options.headers[key]);
    }
    // 註冊xhr物件事件
    xhr.responseType = options.dataType;
    xhr.onprogress = options.onprogress;
    xhr.onuploadprogress = options.onuploadprogress;
    // 開始註冊事件
    // 請求成功
    xhr.onloadend = function () {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        resolve(xhr);
      } else {
        reject({
          errorType: "status_error",
          xhr: xhr
        });
      }
    };
    // 請求超時
    xhr.ontimeout = function () {
      reject({
        errorType: "timeout_error",
        xhr: xhr
      });
    }
    // 請求錯誤
    xhr.onerror = function () {
      reject({
        errorType: "onerror",
        xhr: xhr
      });
    }
    // abort錯誤(未明白,只知道是三種異常中的一種)
    xhr.onabort = function () {
      reject({
        errorType: "onabort",
        xhr: xhr
      });
    }
    // 捕獲異常
    try {
      xhr.send(options.data);
    } catch (error) {
      reject({
        errorType: "send_error",
        error: error
      });
    }
  });
}


// 呼叫示例
_ajax({
  url: 'http://localhost:3000/suc',
  async: true,
  onprogress: function (evt) {
    console.log(evt.position / evt.total);
  },
  dataType: 'text/json'
}).then(
  function (xhr) {
    console.log(xhr.response);
  },
  function (e) {
    console.log(JSON.stringify(e))
  });
複製程式碼

模擬jsonp

// foo 函式將會被呼叫 傳入後臺返回的資料
function foo(data) {
    console.log('通過jsonp獲取後臺資料:', data);
    document.getElementById('data').innerHTML = data;
}
/**
 * 通過手動建立一個 script 標籤傳送一個 get 請求
 * 並利用瀏覽器對 <script> 不進行跨域限制的特性繞過跨域問題
 */
(function jsonp() {
    let head = document.getElementsByTagName('head')[0]; // 獲取head元素 把js放裡面
    let js = document.createElement('script');
    js.src = 'http://domain:port/testJSONP?a=1&b=2&callback=foo'; // 設定請求地址
    head.appendChild(js); // 這一步會傳送請求
})();

// 後臺程式碼
// 因為是通過 script 標籤呼叫的 後臺返回的相當於一個 js 檔案
// 根據前端傳入的 callback 的函式名直接呼叫該函式
// 返回的是 'foo(3)'
function testJSONP(callback, a, b) {
  return `${callback}(${a + b})`;
}

複製程式碼

模擬釋出訂閱模式

class Pubsub {
    constructor() {
        this.handles = {}
    }
    subscribe(type, handle) {
        if (!this.handles[type]) {
            this.handles[type] = []
        }
        this.handles[type].push(handle)
    }
    unsubscribe(type, handle) {
        let pos = this.handles[type].indexOf(handle)
        if (!handle) {
            this.handles.length = 0
        } else {
            ~pos && this.handles[type].splice(pos, 1)
        }
    }
    publish() {
        let type = Array.prototype.shift.call(arguments)
        this.handles[type].forEach(handle => {
            handle.apply(this, arguments)
        })
    }
}

const pub = new Pubsub()
pub.subscribe('a', function() {console.log('a', ...arguments)})
pub.publish('a', 1, 2, 3)
// a 1 2 3
複製程式碼

利用setTimeout模擬setInterval

setTimeout的方法裡面又呼叫了一次setTimeout,就可以達到間歇呼叫的目的。 那為什麼建議使用setTimeout代替setInterval呢?setTimeout式的間歇呼叫和傳統的setInterval間歇呼叫有什麼區別呢?

區別在於,setInterval間歇呼叫,是在前一個方法執行前,就開始計時,比如間歇時間是500ms,那麼不管那時候前一個方法是否已經執行完畢,都會把後一個方法放入執行的序列中。這時候就會發生一個問題,假如前一個方法的執行時間超過500ms,加入是1000ms,那麼就意味著,前一個方法執行結束後,後一個方法馬上就會執行,因為此時間歇時間已經超過500ms了。

“在開發環境下,很少使用間歇呼叫(setInterval),原因是後一個間歇呼叫很可能在前一個間歇呼叫結束前啟動”

簡單版

遞迴呼叫setTimeout函式即可

警告:在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當一個函式必須呼叫自身的時候, 避免使用 arguments.callee(), 通過要麼給函式表示式一個名字,要麼使用一個函式宣告.

var executeTimes = 0;
var intervalTime = 500;
//var intervalId = null;


setTimeout(timeOutFun,intervalTime);

function timeOutFun(){
    executeTimes++;
    console.log("doTimeOutFun——"+executeTimes);
    if(executeTimes<5){
        setTimeout(arguments.callee,intervalTime);
    }
}


<!--// 放開下面的註釋執行setInterval的Demo-->
<!--intervalId = setInterval(intervalFun,intervalTime);-->
<!--function intervalFun(){-->
<!--    executeTimes++;-->
<!--    console.log("doIntervalFun——"+executeTimes);-->
<!--    if(executeTimes==5){-->
<!--        clearInterval(intervalId);-->
<!--    }-->
<!--}-->

複製程式碼

增強版

let timeMap = {}
let id = 0 // 簡單實現id唯一
const mySetInterval = (cb, time) => {
  let timeId = id // 將timeId賦予id
  id++ // id 自增實現唯一id
  let fn = () => {
    cb()
    timeMap[timeId] = setTimeout(() => {
      fn()
    }, time)
  }
  timeMap[timeId] = setTimeout(fn, time)
  return timeId // 返回timeId
}


const myClearInterval = (id) => {
  clearTimeout(timeMap[id]) // 通過timeMap[id]獲取真正的id
  delete timeMap[id]
}
複製程式碼

Promise的模擬實現

Promise的實現

  • 對於實現then方法,我們需要考慮到非同步的情況,即當resolvesetTimeout內執行,thenstate還是pending狀態,我們就需要在then呼叫的時候,將成功和失敗存到各自的陣列,一旦rejectresolve,就呼叫它們
  • 另外一個要注意的地方是如何實現then方法的鏈式呼叫,我們預設在第一個then方法裡返回一個promise,原始碼中規定了一種方法,就是在then方法裡面返回一個新的promise,稱為promise2:promise2=new Promise((resolve,reject)=>{})
    • 將這個promise2返回的值傳遞到下一個then
    • 如果返回一個普通的值,則將普通的值傳遞給下一個then
  • resolvePromise函式的實現是一個關鍵點;promise規範中規定onFullfilled()onRejected()的值,即第一個then返回的值,叫做x,判斷x的函式叫做resolvePromise。具體地, 首先,要看x是不是promise。如果是promise,則取它的結果,作為新的promise2成功的結果如果是普通值,直接作為promise2成功的結果所以要比較x和promise2resolvePromise的引數有promise2(預設返回的promise)、x(我們自己return的物件)、resolverejectresolverejectpromise2
class Promise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    try{
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled,onRejected) {
    // onFulfilled如果不是函式,就忽略onFulfilled,直接返回value
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    // onRejected如果不是函式,就忽略onRejected,直接扔出錯誤
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    let promise2 = new Promise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        // 非同步
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'rejected') {
        // 非同步
        setTimeout(() => {
          // 如果報錯
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          // 非同步
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          // 非同步
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    // 返回promise,完成鏈式
    return promise2;
  }
}



function resolvePromise(promise2, x, resolve, reject){
  // 迴圈引用報錯
  if(x === promise2){
    // reject報錯
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  // 防止多次呼叫
  let called;
  // x不是null 且x是物件或者函式
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      // A+規定,宣告then = x的then方法
      let then = x.then;
      // 如果then是函式,就預設是promise了
      if (typeof then === 'function') { 
        // 就讓then執行 第一個引數是this   後面是成功的回撥 和 失敗的回撥
        then.call(x, y => {
          // 成功和失敗只能呼叫一個
          if (called) return;
          called = true;
          // resolve的結果依舊是promise 那就繼續解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          // 成功和失敗只能呼叫一個
          if (called) return;
          called = true;
          reject(err);// 失敗了就失敗了
        })
      } else {
        resolve(x); // 直接成功即可
      }
    } catch (e) {
      // 也屬於失敗
      if (called) return;
      called = true;
      // 取then出錯了那就不要在繼續執行了
      reject(e); 
    }
  } else {
    resolve(x);
  }
}
複製程式碼

resolve、reject、race與race方法的實現

//reject方法
Promise.reject = function(val){
  return new Promise((resolve,reject)=>{
    reject(val)
  });
}
//race方法 
Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(獲取所有的promise,都執行then,把結果放到陣列,一起返回)
Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
}
複製程式碼

後記:

具體的一些程式碼實現請前往github專案主頁,如果大家覺得對你有幫助的話,請fork一下這個專案,搬磚不易,後期的關於JavaScript,node和框架的原始碼實現都會在github上更新,感謝你的閱讀。

前端路由

hash方式

class Routers {
  constructor() {
    // 儲存hash與callback鍵值對
    this.routes = {};
    // 當前hash
    this.currentUrl = '';
    // 記錄出現過的hash
    this.history = [];
    // 作為指標,預設指向this.history的末尾,根據後退前進指向history中不同的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    // 預設不是後退操作
    this.isBack = false;
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    if (!this.isBack) {
      // 如果不是後退操作,且當前指標小於陣列總長度,直接擷取指標之前的部分儲存下來
      // 此操作來避免當點選後退按鈕之後,再進行正常跳轉,指標會停留在原地,而陣列新增新hash路由
      // 避免再次造成指標的不匹配,我們直接擷取指標之前的陣列
      // 此操作同時與瀏覽器自帶後退功能的行為保持一致
      if (this.currentIndex < this.history.length - 1)
        this.history = this.history.slice(0, this.currentIndex + 1);
      this.history.push(this.currentUrl);
      this.currentIndex++;
    }
    this.routes[this.currentUrl]();
    console.log('指標:', this.currentIndex, 'history:', this.history);
    this.isBack = false;
  }
  // 後退功能
  backOff() {
    // 後退操作設定為true
    this.isBack = true;
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    location.hash = `#${this.history[this.currentIndex]}`;
    this.routes[this.history[this.currentIndex]]();
  }
}

複製程式碼

History方式

class Routers {
  constructor() {
    // 儲存hash與callback鍵值對
    this.routes = {};
    // 當前hash
    this.currentUrl = '';
    // 記錄出現過的hash
    this.history = [];
    // 作為指標,預設指向this.history的末尾,根據後退前進指向history中不同的hash
    this.currentIndex = this.history.length - 1;
    this.refresh = this.refresh.bind(this);
    this.backOff = this.backOff.bind(this);
    // 預設不是後退操作
    this.isBack = false;
    window.addEventListener('load', this.refresh, false);
    window.addEventListener('hashchange', this.refresh, false);
  }

  route(path, callback) {
    this.routes[path] = callback || function() {};
  }

  refresh() {
    this.currentUrl = location.hash.slice(1) || '/';
    if (!this.isBack) {
      // 如果不是後退操作,且當前指標小於陣列總長度,直接擷取指標之前的部分儲存下來
      // 此操作來避免當點選後退按鈕之後,再進行正常跳轉,指標會停留在原地,而陣列新增新hash路由
      // 避免再次造成指標的不匹配,我們直接擷取指標之前的陣列
      // 此操作同時與瀏覽器自帶後退功能的行為保持一致
      if (this.currentIndex < this.history.length - 1)
        this.history = this.history.slice(0, this.currentIndex + 1);
      this.history.push(this.currentUrl);
      this.currentIndex++;
    }
    this.routes[this.currentUrl]();
    console.log('指標:', this.currentIndex, 'history:', this.history);
    this.isBack = false;
  }
  // 後退功能
  backOff() {
    // 後退操作設定為true
    this.isBack = true;
    this.currentIndex <= 0
      ? (this.currentIndex = 0)
      : (this.currentIndex = this.currentIndex - 1);
    location.hash = `#${this.history[this.currentIndex]}`;
    this.routes[this.history[this.currentIndex]]();
  }
}
複製程式碼

參考資料:

相關文章