前端核心知识整理之手写代码篇

手写代码一直是自己认为需要时常拿出来勤加练习,打好编程语言的基本功的极佳方法。通过手写代码的练习,以更深刻地理解 javascript 语言底层运行原理和机制,在编程开发中更灵活地加以运用和避免踩坑,快速高效地定位问题和解决问题。

常见的手写代码:

  • (1). JavaScript 原生方法的实现: new/ call / apply / bind / instanceof / JSON.stringify / JSON.parse / 柯里化;
  • (2). 数组相关的方法:数组去重 / 扁平化 / 深浅拷贝 / 极值;
  • (3). 其他场景:防抖节流 / Promise / 继承 / 创建对象;
  • (4). 框架:Express / React-Router / Redux;

数组去重

一维数组去重最原始的方法就是使用双层循环,分别循环原始数组和新建数组;或者可以通过 indexOf 简化内层循环;或者将原始数组排序完再来去重,这样会减少一层循环,只需要比较前后两个数即可;

也可以使用 ES5ES6 的方法来简化去重的写法,比如可以使用 filter 来简化内层循环,或者使用 SetMap 、扩展运算符等更简单的方法,但效率上应该不会比原始方法好。

二维数组的去重可以在上面方法的基础上再判断元素是不是数组,如果是的话,就进行递归处理。

双层循环

const arr = [1, 1, "1", "1"];
const unique = (arr) => {
    let res = [];
    let arrLen = arr.length;
    for (let i = 0; i < arrLen; i++) {
        let resLen = res.length;
        for (var j = 0; j < resLen; j++) {
            if (arr[i] === res[j]) {
                break;
            }
        }
        if (j === resLen) {
            res.push(arr[i]);
        }
    }
    return res;
}
console.log(unique(arr)); // [1, "1"]

利用 indexOf

const arr = [1, 1, "1"];
const unique = (arr) => {
    const res = [];
    for (let i = 0, len = arr.length; i < len; i++) {
        let current = arr[i];
        if (res.indexOf(arr[i]) === -1) { // arr.indexOf(item) === -1 表示 item 不存在于 arr 中
            res.push(arr[i])
        }
    }
    return res;
}
console.log(unique(arr));

排序后去重

const arr = [2, 2, "2"];
const unique = (arr) => {
    let res = [];
    let sortedArr = arr.concat().sort(); //  拷贝 arr, 并排序
    let seen; // 临时存储的元素
    for (let i = 0, len = sortedArr.length; i < len; i++) {
        // 如果是第一个元素或者相邻的元素不同
        console.log('seen:', seen);
        if (!i || seen !== sortedArr[i]) {
            res.push(sortedArr[i])
        }
        seen = sortedArr[i];
    }
    return res;
}
console.log(unique(arr));

filter 可以用来简化外层循环

使用 indexOf

const arr = [1, 2, 1, 1, '1'];
// arr.indexOf(item) 为索引
const unique = (arr) => arr.filter((item, index, arr) => arr.indexOf(item) === index);
console.log(unique(arr)); // [1, 2, "1"]

排序去重

const arr = [1, 2, 1, 1, '1'];
const unique = (arr) => arr.concat().sort().filter((item, index, arr) => !index || item !== arr[index - 1]);
console.log(unique(arr)); // [1,"1",2]

ES6 方法

Set
const arr = [1, 2, 1, 1, '1'];
const unique = (arr) => Array.from(new Set(arr));
const unique = (arr) => [...new Set(arr)];
console.log(unique(arr)); // [1, 2, "1"]
Map
const arr = [1, 2, 1, 1, '1'];
const unique = (arr) => {
    const seen = new Map();
    return arr.filter(item => !seen.has(item) && seen.set(item, 1));
};
console.log(unique(arr)); // [1, 2, "1"]

类型判断

类型判断需要注意以下几点:

  • typeof 对六个基本类型 Undefined/Null/Boolean/Number/String/Object (大写) 返回的结果分别是 undefinded/object/boolean/number/string/object(小写),可以看到 NullObject 类型都返回了 object 字符串;typeof 却能检查出函数类型;

综上所述, typeof 能够检测出六种类型,但不能检测出 Null 类型和 Object 下细分的类型,如 Array / Function / Date / RegExp / 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((item) => classtype["[object " + item + "]"] = item.toLowerCase());
console.log('classtype', classtype);
/*
{
    "[object Boolean]": "boolean",
    "[object Number]": "number",
    "[object String]": "string",
    "[object Function]": "function",
    "[object Array]": "array",
    "[object Date]": "date",
    "[object RegExp]": "regexp",
    "[object Object]": "object",
    "[object Error]": "error"
}
*/
const type = (obj) => {
    //  解决 IE6 中 null 和 undefined 会被 Object.prototype.toString 识别成 [ojbect 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 === "ojbect" || typeof obj === "function" ? classtype[Ojbect.prototype.toString.call(obj)] || "object" : typeof obj;
}

判断空对象

判断是否有属性, for 循环一旦执行,就说明有属性,此时返回 false

const isEmptyObject = (obj) => {
    let name;
    for (name in obj) {
        return fasle;
    }
    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} 即可。

判断 Window 对象

Window 对象有一个 window 属性指向自身,可以利用这个特性来判断是否是 Window 对象。

const isWindow = (obj) => {
    return obj != null && obj === obj.window;
}
console.log(isWindow(window)); // true

判断数组

isArray 是数组类型内置的数据类型判断函数,但是会有兼容性问题,一个 polyfill 如下

// isArray is a Function, 判断是否存在 isArray
const isArray = Array.isArray || (arr => Object.prototype.toString.call(arr) === '[object Array]');
console.log(isArray([]));

判断类数组

jQuery 实现 isArrayLike , 数组和类数组都会返回 true 。如果 isArrayLike 返回 true ,至少需要满足以下三个条件之一:

  1. 是数组;
  2. length === 0 需要做判断,例如下面的情况下,若不做判断,会打印 false,然而arguments 肯定是一个类数组对象,应该返回 true
function fn() {
    console.log(isArrayLike(arguments));
}
fn(); // false
  1. length 属性是大于 0 的数字类型,并且obj[length-1] 必须存在
const isArrayLike = (obj) => {
    // obj 必须有 length 属性
    let length = !!obj && "length" in obj && obj.length;
    let typeRes = type(obj);
    // 排除掉函数和 Window 对象
    if (typeRes === "function" || isWindow(obj)) {
        return;
    }
    return typeRes === "array" || length === 0 || (typeof length === "number" && length > 0 && (length - 1) in obj);
}

判断 NaN

判断一个数是不是 NaN 不能单纯地使用 === 来判断,因为 NaN 不与任何数相等,包括自身。注意:在 ES6isNaN 中只有值为数字类型使用 NaN 才会返回 true

function isNaN(value) {
    return Number.isNaN(Number(value));
}
console.log(isNaN("1"));
console.log(isNaN(1));
console.log(isNaN(NaN));

判断 DOM 元素

利用 DOM 对象特有的 nodeType 属性

function isElement(obj) {
    // !! 两次感叹号将值转换为布尔值
    return !!(obj && obj.nodeType === 1);
}

判断 arguments 对象

低版本的浏览器中 arguments 对象通过 Object.prototype.toString 判断后返回的是 [object Object] ,所以需要兼容。

function isArguments(obj) {
    return Object.prototype.toString.call(obj) === '[object Arguments]';
}

深浅拷贝

如果是数组,可以通过 slice / concat 返回一个新数组的特性来实现浅拷贝;对于深拷贝,可以利用 JSON.parseJSON.stringify 来实现,但是有一个问题,不能拷贝函数(此时拷贝后返回的数组为 null )。

对于通用的拷贝数组或对象,浅拷贝只复制一层,实现方式可以通过 Ojbect.assign() ,注意当目标对象只有一层时,也是深拷贝,数组的 concat / slice 方法均为深拷贝。

深拷贝是完全拷贝、多层拷贝,实现方式有 loadash 库, jQueryextend 方法、 JSON.parse(JSON.stringify(obj)) 或者手写递归方法等。

浅拷贝

思路:遍历对象,然后把属性和值都放在一个新的对象里就好了。

const shallowCopy = (obj) => {
    // 只拷贝对象
    if (typeof obj !== "object") return;
    // 根据 obj 的类型判断是新建一个数组还是对象
    let newObj = obj instanceof Array ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
深拷贝

思路:在拷贝的时刻判断一下属性值的类型,如果是对象,就递归调用深拷贝函数。

const deepCopy = (obj) => {
    if (typeof obj !== "object") return;
    let newObj = obj instanceof Array ? [] : {};
    for (let key in obj) {
        newObj[key] = typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key];
    }
    return newObj;
}

扁平化

递归:循环数组元素,如果还是数组,就递归调用该方法。

const arr = [1, [2, [3, 4]]];
const flatten = (arr) => {
    let result = [];
    for (let 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)); // [1, 2, 3, 4]

toString()

如果数组元素都是数字,可以使用该方法

const arr = [1, [2, [3, 4]]];
const flatten = (arr) => {
    //  +item 会使字符串发生类型转换
    return arr.toString().split(',').map(item => +item);
}
console.log(flatten(arr));

reduce()

// 扁平化多维数组
const arr = [1, [2, [3, 4]]];
const flatten = (arr) => {
    while (arr.some(item => Array.isArray(arr[item]))) {
        arr = [].concat(...arr);
    }
    return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4]

柯里化

通用版

const curry = (fn, args) => {
    let len = fn.length;
    let args = args || [];
    return function() {
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if (newArgs.length < len) {
            return curry.call(this, fn, newArgs);
        } else {
            return fn.apply(this, newArgs);
        }
    }
}

function multiFn(a, b, c) {
    return a * b * c
}
const 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);
curryTest(1, 2)(4)(3);
curryTest(1, 2)(3, 4);

防抖与节流

防抖
const debounce = (fn, wait) => {
    let timeout = null;
    return () => {
        if (timeout !== null) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(fn, wait);
    }
}
// 处理函数
function handle() {
    console.log(Math.random());
}
//  滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
节流

利用时间戳实现

const throttle = (func, delay) => {
    let prev = 0;
    return function() {
        const context = this;
        const args = arguments;
        let now = Date.now();
        if (now - prev > delay) {
            func.apply(context, args);
            prev = Date.now();
        }
    }
}
const handle = () => {
    console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
利用定时器实现
 const throttle = (func, delay) => {
     let timer = null;
     return function() {
         const context = this;
         const args = arguments;
         if (!timer) {
             timer = setTimeout(function() {
                 func.apply(context, args);
                 timer = null;
             }, delay);
         }
     }
 }
 const handle = () => {
     console.log(Math.random());
 }
 window.addEventListener('scroll', throttle(handle, 1000));
利用时间戳+定时器

节流中利用时间戳或定时器都是可以的。更精确地,可以用时间戳+定时器,当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数。

const throttle = (func, delay) => {
    let timer = null;
    let startTime = 0;
    return function() {
        let currentTime = Date.now();
        let remaining = delay - (currentTime - startTime);
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        if (remaining <= 0) {
            func.apply(context, args);
            startTime = Date.now();
        } else {
            timer = setTimeout(func, remaining);
        }
    }
}
const handle = () => {
    console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));

模拟 new

  • new 产生的实例可以访问 Constructor 里的属性,也可以访问到 Constructor.prototype 中的属性,前者可以通过 apply 来实现,后者可以通过将实例的 proto 属性指向构造函数的 prototype 来实现;
  • 我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么;
function New() {
    let obj = new Object();
    // 取出第一个参数,就是我们要传入的构造函数;此外因为 shift 会修改原数组,所以 arguments 会被去掉第一个参数
    Constructor = [].shift.call(arguments);
    // 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
    obj.__proto__ = Contructor.prototype;
    // 使用 apply 改变构造函数的 this,使其指向新建的对象,这样 obj 就可以访问到构造
    const ret = Constructor.apply(obj, arguments);
    // 返回 obj
    return typeof ret === 'ojbect' ? ret : obj;
}

模拟 call

  • call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法
  • 模拟的步骤是: 将函数设为对象的属性 -> 执行该函数 -> 删除该函数
  • this 参数可以传 null , 当为 null 的时候,视为指向 ``window
  • 函数是可以有返回值的
简单版
const foo = {
    value: 1,
    bar: function() {
        console.log(this.value);
    }
}
foo.bar(); // 1
完善版
Function.prototype.call2 = function() {
    let context = context || window;
    context.fn = this; // 将函数设为 context 对象的属性
    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');

模拟 apply

  • apply() 的实现和 call() 类似,只是参数形式不同
  • apply() 方法在使用一个指定的 this 值和一个包含若干参数的数组的前提下调用某个函数或方法
  • 模拟的步骤是: 将函数设为对象的属性 -> 执行该函数 -> 删除该函数
  • this 参数可以传 null , 当为 null 的时候,视为指向 ``window
  • 函数是可以有返回值的
Function.prototype.apply2 = function(context = window) {
    context.fn = this;
    let result;
    // 判断是否有第二个参数
    result = arguments[1] ? context.fn(...arguments[1]) : context.fn();
    delete context.fn;
    return result;
}

模拟 bind

Function.prototype.bind2 = function(context) {
    let self = this;
    let args = Array.prototype.slice.call(arguments);
    let fNop = function() {};
    let fBound = function() {
        let bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNop ? this : context, args.concat(bindArgs));
    }
}

模拟 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(value[, reviver])

用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数可以在返回之前对所得的对象执行操作变换。

利用 eval

function jsonParse(opt) {
    return eval('(' + opt + ')');
}
jsonParse(jsonStringify({
    x: 5
})); // Ojbect {x: 5}
jsonParse(jsonStringify([1, "false", false])); // [1, "false", false]
jsonParse(jsonStringify({
    b: undefined
})); // Ojbect  {b: "undefined"}

避免在不必要的情况下使用 eval , eval 是一个危险的函数,它执行的代码拥有执行者的权利。如果你用 eval() 运行的字符串代码被恶意方操控修改,最终可能会在你的网页/扩展程序的权限下,运行恶意代码。

利用 new Function()

Functioneval 有相同的的字符串参数特性, evalFunction 都有着动态编译 js 代码的作用,但在实际编程中并不推荐。

var func = new Function(arg1, arg2, ..., functionBody)
let jsonStr = '{"age": "20", "name": "jack"}';
let json = (new Function('return' + jsonStr))();

创建对象

创建自定义对象最简单的方式就是创建一个 Ojbect 实例,然后再为它添加属性和方法,早期的开发人员经常使用这种模式来创建对象,后来对象字面量的方法成了创建对象的首选模式。虽然 ojbect 构造函数 或者 对象字面量 的方法都可以用来创建对象,但是这些方法使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这个问题,人们开始使用各种模式来创建对象。

在这些模式中一般推荐使用四种方式,包括 构造函数模式原型模式构造函数和原型组合模式动态原型模式 , 其他的方式,包括 工厂模式寄生构造函数模式稳妥构造函数模式 平时使用较少。而这些方式中,用的最多的是 构造函数和原型组合模式动态原型模式

构造函数和原型组合模式

优点:

  • 解决了原型模式对于引用对象的缺点
  • 解决了原型模式无法传递参数的缺点
  • 解决了构造函数无法共享方法的缺点
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"

动态原型模式

优点:

  • 可以在初次调用构造函数的时候就完成对原型对象的修改;
  • 修改能体现在所有的实例中;
function Person(name) {
    this.name = name;
    // 检测 say 是否为一个函数
    // 实际上只有在第一次没有创建的时候才会在原型上添加 say 方法
    // 因为构造函数执行时,里面的代码都会执行一遍,而原型只有一个就行,不用每次都重复,所以仅在第一执行时生成一个原型,后面执行就不必生成,所以就不会执行 if 包裹的函数。
    // 其次为什么不能再使用字面量的写法,我们都知道,使用构造函数其实是把 new 出来的对象作用域绑定在构造函数上,而字面量的写法,会重新生成一个新对象,就切断了两者的联系。
    if (typeof this.say !== "function") {
        Person.prototype.say = function() {
            alert(this.name);
        }
    }
}

继承

原型继承 不仅会带来引用缺陷,而且我们也无法为不同的实例初始化继承来的属性; 构造函数继承 方式可以避免类式继承的缺陷,但是无法获取到父类的共有方法,就是是通过原型 prototype 绑定的方法; 组合继承 解决了上面两种方式存在的问题,但是它调用了两次父类的构造函数; 寄生组合式继承 强化的部分就是在组合继承的基础上减少一次多余的调用父类的构造函数。推荐使用 组合继承方式寄生组合式继承ES6 extends 继承 ,建议在实际生产中直接使用 ES6 extends 继承

组合继承
// 声明父类
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('white');
var dog2 = new Dog('black');

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

console.log(dog2.type); // ['pig', 'cat']
console.log(dog2.color); // 'black'
dog.greet('汪汪'); // '汪汪'

注: 组合继承利用上面的方式会使得调用两次父类的构造函数,其实我们可以通过 Dog.prototype = Animal.prototype; Dog.prototype.constructor = Dog 来优化组合继承,当然终极优化方式就是下面的寄生组合方式。

寄生组合继承
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';
}
// 下面两行代码是核心
// 拷贝 Animal的原型并赋值给 Dog的原型
Dog.prototype = Object.create(Animal.prototype);
// 修复 Dog 原型上的 constructor 属性
Dog.prototype.constructor = Dog;

Dog.prototype.getName = function() {
    console.log(this.name);
}
let dog = new Dog('白色');
let dog2 = new Dog('黑色');
dog.type.push('dog');
console.log(dog.color); //'白色'
console.log(dog.type); // ["pig", "cat", "dog"]

console.log(dog2.color); // '黑色'
console.log(dog2.type); // ["pig", "cat"]
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); // '黑色'

实现 map

核心要点:1、回调函数的参数有哪些,返回值如何处理? 2、不修改原来的数组。

Array.prototype.myMap = function(fn, context) {
    var arr = Array.prototype.slice.call(this); // 由于是 ES5 就不用 ... 展开符了
    var mappedArr = [];
    for (var i = 0; i < arr.length; i++) {
        mappedArr.push(fn.call(context, arr[i], i, this));
    }
    return mappedArr;
}

实现 Object.create

function create(proto) {
    function F() {};
    F.prototype = proto;
    F.prototype.constructor = F;
    return new F();
}

模拟 ajax

  • ajax 请求过程:创建 XMLHttpRequest 对象、连接服务器、发送请求、接收响应数据
  • 创建后的XMLHttpRequest 对象实例拥有很多方法和属性
    • open 方法类似于初始化,并不会发起真正的请求;send 方法发送请求,并接受一个可选参数
    • 当请求方式为post 时,可以将请求体的参数传入; 当请求方式为get 时,可以不传或传入 null
    • 不管是get 还是post,参数都需要通过 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 浏览器,那么 XMLHttpRequest 是不存在的,应该调用 ActiveObject;
    let xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveObject('Microsoft.XMLHTTP');
    if (params.type === 'GET') {
        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);
            }
        }
    }
    // 第二种写法,当 xhr.readyState === 4 时,说明请求成功返回了,进行成功回调
    xhr.onreadystatechange = funciton() {
        if (xhr.readyState === 4) {
            // 进行 onload 里面的处理函数
            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);
                }
            }
        }
    }
}

promise

//  使用 promise 实现一个简单的 ajax
/**
 * 首先,可能会使用到的 xhr 方法或者属性
 * onloadstart 开始发送时触发
 * onloadend 发送结束时触发,无论成功与否
 * onload 得到响应时
 * onprogress 从服务器上下载数据,每 50ms 触发一次
 * onuploadprogress 上传到服务器的回调
 * onerror 请求错误时触发
 * onabort 调用中止时触发
 * status 返回状态码
 * setRequestHeader 设置请求头
 * responseType 请求传入的参数
 **/
//  默认的 ajax 参数
let ajaxDefaultOptions = {
    url: '#', // 请求地址,默认为空
    method: 'GET', // 请求方式,默认为 GET 请求
    async: true, // 请求是同步还是异步,默认为异步
    timeout: 0, // 请求的超时时间
    dataType: 'text', // 请求的数据格式,默认为 text
    data: null, // 请求的参数,默认为空
    headers: {}, // 请求头,默认为空
    onprogess: 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,则用外部的 xhr,否则创建一个
let xhr = options.xhr || new XMLHttpRequest();
// return promise 对象
return new Promise(function(resolve, reject) {
    xhr.open(options.method, options.url, option.async);
    xhr.timeout = option.timeout;
    // 设置请求头
    for (const key in options.headers) {
        xhr.setRequestHeader(key, options.header[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.ontimeout = function() {
        reject({
            errorType: "timeout_error",
            xhr
        });
    };
    // 请求错误
    xhr.onerror = function() {
        reject({
            errorType: "onerror",
            xhr
        })
    };
    // abort 错误,即异常
    xhr.onabort = function() {
        reject({
            errorType: "onabort",
            xhr
        });
    };
    // 捕获异常
    try {
        xhr.send(options.data);
    } catch (error) {
        reject({
            errorType: "send_error",
            error
        });
    }
});

// 调用示例
_ajax({
    url: 'http://localhost:3001/static',
    async: true,
    onprogress: function(e) {
        console.log(e.position / e.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.getElementByTagName('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 函数即可。

警告:在严格模式下,ES5 禁止使用 arguments.callee() 。当一个函数必须调用自身的时候,避免使用 arguments.callee() ,通常要么给函数表达式取一个名字,要么使用一个函数声明。

var executeTimes = 0;
var intervalTime = 500;
// var intervalId = null;
setTimeout(timeOutFun, intervalTime);

function timeOutFun() {
    executeTimes++;
    console.log("doTimeOutFun--" + excuteTimes);
    if (executeTimes < 5) {
        setTimeout(arguments.callee, intervalTime);
    }
}
// 放开下面的注释运行 setInterval 的 Demo
// intervalId = setInterval(intervalFun, intervalTime);

// function intervalFun() {
//     executeTimes++;
//     console.log("doIntervalFun--" + excuteTimes);
//     if (executeTimes == 5) {
//         clearInterval(intervalId);
//     }
// }

增强版

let timeMap = {};
let id = 0; // 简单实现 id 唯一
const mySetInterval = (cb, time) => {
    let timeId = id; // 将 id 赋值给 timeId
    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 的模拟实现

  • 对于实现 then 方法,需要考虑到异步情况,即当 resolve 在 setTimeout 内执行,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 成功的结果。所以要对比 xpromise2resolvePromise的参数有 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 = 'fullfilled';
                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(onFullFilled, onRejected) {
        // onFullFilled 如果不是函数,就忽略 onFullFilled,直接返回 value
        onFullFilled = typeof onFullFilled === 'function' ? onFullFilled : value => value;
        // onRejected 如果不是函数,就忽略 onRejected,直接扔出错误
        onRejected = typeof onRejected === 'function' ? onRejected : err => {
            throw err
        };
        let promise2 = new Promise((resolve, reject) => {
            if (this.state === 'fullfilled') {
                // 异步
                setTimeout(() => {
                    try {
                        let x = onFullFilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (err) {
                        reject(err);
                    }
                }, 0);
            }
            if (this.state === 'rejected') {
                // 异步
                setTimeout(() => {
                    // 如果报错
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (err) {
                        reject(err);
                    }
                }, 0);
            }
            if (this.state === 'pending') {
                this.onResolvedCallbacks.push(() => {
                    // 异步
                    setTimeout(() => {
                        try {
                            let x = onFullFilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (err) {
                            reject(err);
                        }
                    }, 0);
                });
                this.onRejectedCallbacks.push(() => {
                    // 异步
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (err) {
                            reject(err);
                        }
                    }, 0);
                });
            }
        });
        //  返回 promise2,完成链式
        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, x, resolve, reject)
                }, err => {
                    // 成功和失败只能调用一个
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                resolve(x); // 直接成功即可
            }
        } catch (err) {
            // 也属于失败
            if (called) return;
            called = true;
            // 取 then 出错了就不要再继续执行了
            reject(err);
        }
    } else {
        resolve(x);
    }
}

resolverejectracerace 方法的实现

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

前端路由

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.addEventListenner('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]]();
    }
}

图片懒加载

<script>
    // 获取所有的图片路径
    const imgs = document.getElementByTagName('img');
    // 获取可视区的高度
    const viewHeight = window.innerHeight || document.documentElement.clientHeight;
    // num 用于统计当前显示到哪一张图片,避免每次都从第一张图片开始检查是否露出
    let num = 0;

    function lazyLoad() {
        for (let i = num; i < imgs.length; i++) {
            // 用可视区域高度减去元素顶部距离可视区域顶部的高度
            let distance = viewHeight - imgs[i].getBoundingClientRect().top;
            // 如果可视区域高度大于等于元素顶部距离可是去预订部的高度,说明元素露出
            if (distance > 0) {
                // 给元素写入真实的src,显示图片
                img[i].src = imgs[i].getAttribute('data-src');
                // 前 i 张图片已经加载完毕,下次从第 i+1 张开始检查是否露出
                num = i + 1;
            }
        }
    }
    // 监听 Scroll 事件
    window.addEventListener('scroll', lazyLoad, false);
</script>