前端面試之手寫代碼

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

數組去重

(一維)數組去重最原始的方法就是使用雙層循環,分別循環原始數組和新建數組;或者咱們可使用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能夠用來簡化外層循環java

使用indexOf:node

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));
複製代碼

排序去重:jquery

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:git

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

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

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

再簡化下github

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))
}
複製代碼

類型判斷

類型判斷須要注意如下幾點ajax

  • typeof對六個基本數據類型UndefinedNullBooleanNumberStringObject(大寫)返回的結果是npm

    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]]();
  }
}
複製代碼

參考資料:

相關文章
相關標籤/搜索