Skip to content

Ajax 的原生写法

js
// GET, POST, HEAD, PUT, DELETE, CONNECT, OPTIONS, TRACE
const ajax = {
    get: function (url, options) {
        const xhr = new XMLHttpRequest();
        let params = options.params || {};

        for (let key in params) {
            if (!params.hasOwnProperty(key)) return;
            url += (/\?/.test(url) ? '&' : '?') + key + '=' + params[key];
        }

        // 若需传参,则再 open 之前完成相关 data 序列化
        xhr.open('GET', url, true);

        // setRequestHeader 方法需在 open() 之后,send() 之前调用
        xhr.setRequestHeader('token', options.headers.token);

        xhr.onreadystatechange = function () {
            if (
                xhr.readyState === 4
                    && (xhr.status === 200 || xhr.status === 304)
            ) {
                options.callback && options.callback.call(this, xhr.responseText);
            }
        }

        xhr.send();
    },
    post: function (url, options) {
        const xhr = new XMLHttpRequest();

        // 语法:xhrReq.open(method, url, async, user, password);
        xhr.open('POST', url, true);

        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

        xhr.onreadystatechange = function () {
            if (
                xhr.readyState === 4
                    && (xhr.status === 200 || xhr.status === 304)
            ) {
                options.callback && options.callback.call(this, xhr.responseText);
            }
        }

        xhr.send(options.data || {});
    }
}

对象深拷贝、浅拷贝

在 JS 中,除了基本数据类型,还存在对象、数组这种引用类型。基本数据类型,拷贝是直接拷贝变量的值,而引用类型拷贝的其实是变量的地址。

浅拷贝:

  1. 通过 Object.assign 实现
  2. 通过 ES6 展开运算符 ... 来实现 const obj = { ...copy }

深拷贝:

通常可以通过 JSON.parse(JSON.stringify(object)) 来解决:

js
/**
 * 存在问题:
 * 1. 会忽略undefined、symbol和函数
 * 2. NaN、Infinity、-Infinity 会被序列化为 null
 * 3. 如果有循环引用的对象,会报错
 */
let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
};

let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是以上方法存在一定的局限性:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

但是在通常情况下,复杂数据都是可以序列化的,所以函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数

js
function deepClone (target, hash = new WeakMap()) { // 额外开辟一个存储空间 WeakMap 来存储当前对象
    // 如果是 null 就不进行拷贝操作
    if (target === null) return target;

    // 处理日期
    if (target instanceof Date) return new Date(target);

    // 处理正则
    if (target instanceof RegExp) return new RegExp(target);

    // 处理 DOM元素
    if (target instanceof HTMLElement) return target;

    // 处理原始类型和函数:不需要深拷贝,直接返回
    if (typeof target !== 'object') return target;

    // 是引用类型的话就要进行深拷贝
    // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
    if (hash.get(target)) return hash.get(target);

    // 创建一个新的克隆对象或克隆数组
    const cloneTarget = new target.constructor();
    // 如果存储空间中没有就存进 hash 里
    hash.set(target, cloneTarget);

    // 引入 Reflect.ownKeys,处理 Symbol 作为键名的情况
    Reflect.ownKeys(target).forEach(key => {
        // 递归拷贝每一层
        cloneTarget[key] = deepClone(target[key], hash);
    });

    // 返回克隆的对象
    return cloneTarget;
}

// test
var o1 = { a: 'a', b: 'b', c: o2 };
var o2 = { d: o1, e: new Date(), f: () => {}, g: undefined, h: null, [Symbol()]: 'symbol' };

deepClone(o2);

未来的深拷贝:

这个 Web API 名称叫 structuredClone(),详情可访问 MDN 和最新的 HTML5 规范:

js
const obj = {
    person: {
        name: 'lin'
    },
};

const newObj = structuredClone(obj);
obj.person.name = 'xxx'; // 改变原来的对象

console.log('原来的对象', obj);
console.log('新的对象', newObj);

console.log('更深层的对象指向同一地址', obj.person == newObj.person); // false

参考资料:

实现页面加载进度条

手动实现 parseInt

parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。

很难完全模拟,比如 字母如何转成数字?

怎么判断两个对象是否相等

  1. 通过 JSON.stringify(obj) 来判断两个对象转后的字符串是否相等
  2. Object.getOwnPropertyNames 获取到两个个对象的所有 key,然后遍历对比(若为引用类型值则 toString 后对比)

实现一个 new 操作符

js
function create(...args) {
    let obj = {};
    let Con = args.shift();

    obj.__proto__ = Con.prototype;

    let result = Con.apply(obj, args);

    return typeof result === "object" ? result : {};
}

实现一个 Array.isArray

js
if (!Array.isArray) {
    Array.isArray = function (o) {
        return Object.prototype.toString.call(o) === "[Object Array]";
    }
}

a 可以同时 == 1 && == 2 && == 3 吗?

  1. 类型转换时劫持
    js
    let a = {
        arr: [3, 2, 1],
        valueOf() {
            return this.arr.pop();
        }
    }
    
    if (a == 1 && a == 2 && a == 3) {
        console.log('Hello world !');
    }
    
    // or
    let a = {
        num: 1,
        valueOf() {
            return this.num++;
        }
    }
    
    if (a == 1 && a == 2 && a == 3) {
        console.log('Hello world !');
    }
  2. 对 getter 劫持
    js
    let a = new Proxy({}, {
        num: 1,
        get() {
            return () => this.num++;
        }
    });
    
    if (a == 1 && a == 2 && a == 3) {
        console.log('Hello world !');
    }
    
    // or
    // 利用 ES6 Generator
    let num = (function*() {
        let i = 0;
        do {
            yield ++i;
        } while (1);
    })();
    // 更简单的写法
    // let num = (function*() {
    //     yield 1;
    //     yield 2;
    //     yield 3;
    // })();
    
    Object.defineProperty(window, "a", {
        get() {
            return num.next().value;
        }
    });
    
    // 类型都相同,可全等
    if (a === 1 && a === 2 && a === 3) {
        console.log("Hello world !");
    }
  3. 正则表达式
    js
    /**
     * 当正则表达式使用 `g` 标志时,可以多次执行 `exec` 方法来查找同一个字符串中的
     * 成功匹配。当你这样做时,查找将从正则表达式的 `lastIndex` 属性指定的位置开始。
     * ( `test()` 也会更新 `lastIndex` 属性)。
     *
     * `lastIndex` 是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始
     * 索引。只有正则表达式使用了表示全局检索的 `g` 标志时,该属性才会起作用。
     */
    let a = {
        reg: /\d/g,
        valueOf() {
            return this.reg.exec(123)[0];
        }
    }
    
    if (a == 1 && a == 2 && a == 3) {
        console.log('Hello world !');
    }

参考资料:

Promise.all 的实现

js
function promiseAll(promiseArray) {
    if (!Array.isArray(promiseArray)) {
        return reject(new Error('传入的参数必须是数组'));
    }

    const result = [];
    const promiseNums = promiseArray.length;
    let counter = 0;

    return new Promise((resolve, reject) => {
        for (let i = 0; i < promiseNums; i++) {
            Promise.resolve(promiseArray[i]).then(resp => {
                counter++;
                result[i] = resp;

                if (counter === promiseNums) resolve(result);
            }).catch(e => reject(e));
        }
    });
}

模拟 ES6 class 继承的实现

js
function xExtend(sub, super) {
    let proto = Object.create(super.prototype);
    proto.constructor = sub;
    sub.prototype = proto;
    sub.super = super.prototype;
}

reduce 的实现

Array.prototype.reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

reducer 函数接收 4 个参数:

  • Accumulator (acc) (累计器)
  • Current Value (cur) (当前值)
  • Current Index (idx) (当前索引)
  • Source Array (src) (源数组)
js
Array.prototype.xreduce = function (callback, acc) {
    result = acc || this[0];

    for (let i = acc ? 0 : 1; i < this.length; i++) {
        result = callback(result, this[i], i, this);
    }

    return result;
}

实现一个数据双向绑定

js
/**
 * Object.defineProperty
 */
const data = { name: 'jimco' };

const input = document.getElementById('input');
const span = document.getElementById('span');

Object.defineProperty(data, 'name', {
    set(newVal) {
        this.store_name = newVal;
        // 数据变更 -> 视图变更
        input.value = newVal;
        span.innerHTML = newVal;
    }
});

// 视图变更 -> 数据变更
input.addEventListener('keyup', function(e) {
    data.name = e.target.value;
});


/**
 * Proxy
 */
const data = { name: 'jimco' };

const input = document.getElementById('input');
const span = document.getElementById('span');

const handler = {
    // get(target, prop) {},
    set(target, prop, value) {
        target[prop] = value;
        // 数据变更 -> 视图变更
        input.value = value;
        span.innerHTML = value;

        return value;
    }
};

const proxy = new Proxy(data, handler);

// 视图变更 -> 数据变更
input.addEventListener('keyup', function(e) {
    proxy.name = e.target.value;
});

getOwnPropertyNames 实现

Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。

js
if (typeof Object.getOwnPropertyNames !== 'function') {
    Object.getOwnPropertyNames = function(o) {
        if (o !== Object(o)) {
            throw TypeError('Object.getOwnPropertyNames called on non-object');
        }

        let props = [];
        for (let key in o) {
            if (Object.prototype.hasOwnProperty.call(o, key)) {
                props.push(key);
            }
        }

        return props;
    };
}

函数柯里化的实现

函数柯里化的本质是将一个参数很多的函数分解成单一参数的多个函数。

实际应用中:

  • 延迟计算(用闭包把传入参数保存起来,当传入参数的数量足够执行函数时,开始执行函数)
  • 动态创建函数(参数不够时会返回接受剩下参数的函数)
  • 参数复用(每个参数可以多次复用)
js
/**
 * 解法一:
 */
const curry = (fn) => {
    return function judge(...args) {
        return args.length === fn.length
            ? fn(...args)
            : (...arg) => judge(...args, ...arg);
    }
}

// 测试:
const sum = (a, b, c, d, e) => a + b + c + d + e;
const currySum = curry(sum);

currySum(1)(2)(3)(4)(5);
currySum(1)(2, 3)(4)(5);
currySum(1, 2, 3)(4, 5);

/**
 * 解法二:
 */
function add(...args) {
    let _add = function () {
        args.push(...arguments);

        return _add;
    }

    _add.toString = function () {
        return args.reduce((a, b) => a + b);
    }

    return _add;
}

add(1)(2)(3)(4)(5) == 15; // true
add(1)(2, 3, 4)(5) == 15; // true
add(1, 2)(3, 4)(5) == 15; // true

async/await 的实现

理解 async 函数需要先理解 Generator 函数,因为 async 函数是 Generator 函数的语法糖。

利用 generator(生成器)分割代码片段。然后我们使用一个函数让其自迭代,每一个 yieldpromise 包裹起来。执行下一步的时机由 promise 来控制:

js
function asyncGenerator(fn) {
    let gen = fn();

    function next(data) {
        let result = gen.next(data);

        if (result.done) return result.value;

        result.value.then(function (data) {
            next(data);
        });
    }

    next();
}

// Test:
function getNum(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num + 1);
        }, 1000);
    });
}

let func = function* () {
    var f1 = yield getNum(1);
    var f2 = yield getNum(f1);
    console.log(f2) ;
};

asyncGenerator(func);

用 Proxy 实现 Vue 的数据劫持

js
function isArray(o) {
    return Object.prototype.toString.call(o) === '[object Array]';
}

function isObject(o) {
    return Object.prototype.toString.call(o) === '[object Object]';
}

class Observables {
    constructor(target, handler = {
        set(target, key, value, receiver) {
            console.log(`数据变更[${key}] -> 视图变更`);
            return Reflect.set(target, key, value, receiver);
        }
    }) {
        if (!isArray(target) && !isObject(target)) {
            throw new TypeError('target 不是数组/对象');
        }

        this._target = JSON.parse(JSON.stringify(target));
        this._handler = handler;

        return new Proxy(this._observable(this._target), this._handler);
    }

    _observable(target) {
        for (let key in target) {
            if (isArray(target[key]) || isObject(target[key])) {
                this._observable(target[key]);
                target[key] = new Proxy(target[key], this._handler);
            }
        }

        return target;
    }
}

// Test:
const ob = new Observables(o);
ob.a.push(3);
ob.c.a = 2;
ob.c.c[0][2].d = 6;
ob.b = 44;

实现一个 Array.prototype.map()

js
Array.prototype.xMap = function (callback, ctx) {
    let result = [];

    for (let i = 0; i < this.length; i++) {
        result.push(callback.call(ctx, this[i], i, this));
    }

    return result;
}

实现一个 Array.prototype.flat()

js
Array.prototype.xFlat = function (depth = 1) {
    const arr = this;

    return depth > 0
        ? arr.reduce((acc, val) => {
            return acc.concat(
                Array.isArray(val) ? val.xFlat(depth - 1) : val
            )
        }, [])
        : arr.slice();
}

Array.prototype.slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括 end),原始数组不会被改变。

JS 解决 0.1 + 0.2 == 0.3 的问题

JS 浮点数计算精度问题是因为某些小数没法用二进制精确表示出来。JS 使用的是 IEEE754 双精度浮点规则。

  1. 利用 ES6 提供的 Number.EPSILON 方法
    js
    // Polyfill
    if (Number.EPSILON === undefined) {
        Number.EPSILON = Math.pow(2, -52);
    }
    
    function numEqual(a, b) {
        // 在这个误差的范围内就可以判定 0.1+0.2 === 0.3 为true
        return Math.abs(a - b) < Number.EPSILON;
    }
  2. 调用 Math.round() 方法四舍五入,或者 Math.toFixed() 保留指定的位数(对精度要求不高可用这种方案)
  3. 将小数转为整数再做计算
    js
    (0.1*10 + 0.2*10) / 10;
    
    // or
    function calcFloatAdd(a, b) {
        let digit = Math.max(
            (a.toString().split('.')[1] || []).length,
            (b.toString().split('.')[1] || []).length
        );
        let num = Math.pow(10, digit);
    
        return (a*num + b*num) / num;
    }

参考资料:

下拉刷新,上拉加载实现原理

下拉刷新

  • 监听原生 touchstart 事件,记录初始位置值 e.touches[0].pageY
  • 监听原生 touchmove 事件并 e.preventDefault(),记录并计算当前滑动的位置与初始位置的差值,大于 0 表示向下拉动,并借助 CSS3 的 translateY 属性使元素跟随手势向下滑动相应的差值,同时也应设置一个允许滑动的最大值
  • 监听原生 touchend 事件,若此时元素滑动达到或超过临界值则触发 callback,同时将 translateY 重设为 0,元素回到初始位置

上拉加载的实现也是基于类似的原理。

**注意:**下拉刷新/上拉加载都需在页面滚动到顶部/底部时才执行,否则会导致滚动不可用

求代码输出,并说出为什么

js
var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
};

obj.push(1);
obj.push(2);
obj.push(3);
console.log(obj);

解析obj 有长度,相当于类数组,调用数组的 push,会在数组的最后加一项,第一次调用,相当于长度变为 3,那么下标为 2 的那一项被赋值为 1,下标是 2,当其作为对象的 key 值的时候,会隐式调用 toString 方法转为字符串 2,则和 obj 本来有的 key=2 相同,原来的 key 为 2 的 value 就被覆盖了。以此类推后面的 2 个 push。

实现 LRU(Least Recently Used, 即最近最少使用) 缓存机制

实现一个 LRU 过期算法的 KV cache,所有 KV 过期间隔相同,满足如下性质:

  • 最多存储 n 对 KV
  • 如果大于 n 个,则随意剔除一个已经过期的 KV
  • 如果没有过期的 KV,则按照 LRU 的规则剔除一个 KV
  • 查询时如果已经过期,则返回空
js
class LRUCache {
    constructor(capacity, intervalTime) {
        this.cache = new Map();
        this.capacity = capacity;
        this.intervalTime = intervalTime;
    }

    get(key) {
        if (!this.cache.has(key)) {
            return null
        }

        const tempValue = this.cache.get(key);
        this.cache.delete(key);

        if (Date.now() - tempValue.time > this.intervalTime) {
            return null;
        }

        this.cache.set(key, { value: tempValue.value, time: Date.now() });
        return tempValue.value;
    }

    put(key, value) {
        if (this.cache.has(key)) {
            this.cache.delete(key);
        }
        if (this.cache.size >= capacity) {
            const keys = this.cache.keys();
            // * Map 是有序的,故最长时间未被访问的值会被第一个迭代到
            this.cache.delete(keys.next().value);
        }
        this.cache.set(key, { value, time: Date.now() });
    }
}

前端实现文件下载

Blob + URL.createObjectURL

js
// 将用户的配置信息导入到一个 json 文件中并下载
const config = {
    name: 'lsqy',
    password: 'yourpassword',
    ak: 'XXXXXXXXXX',
    sk: 'XXXXXXXXXX'
};

const blobContent = new Blob(
    [JSON.stringify(config, null, 2)],
    {type : 'application/json'}
);

const blobUrl = window.URL.createObjectURL(blobContent);

downloadFileByBlob(blobUrl, 'config.json');

function downloadFileByBlob(blobUrl, filename) {
    const eleLink = document.createElement('a');
    eleLink.download = filename;
    eleLink.style.display = 'none';
    eleLink.href = blobUrl;
    // 触发点击
    document.body.appendChild(eleLink);
    eleLink.click();
    // 然后移除
    document.body.removeChild(eleLink);
}

实现一个异步调用队列

js
let queue = function(funcs, scope){
    (function next() {
        if (funcs.length > 0) {
            funcs.shift().apply(scope, [next].concat(Array.prototype.slice.call(arguments, 0)));
        }
    })();
}

// Example
let obj = { value: null };

queue([
    function (callback) {
        var me = this;
        setTimeout(function () {
            me.value = 10;
            callback(20);
        });
    },
    function (callback, add) {
        console.log(this.value + add);
        callback();
    },
    function () {
        console.log(obj.value);
    }
], obj);

setInterval 计时器误差解决方案

js
let startTime = new Date().getTime();

setInterval(function () {
    let i = 0;

    // 耗时任务
    while(i++ < 2000000000);

    console.log(new Date().getTime() - startTime);
}, 1000);

如以上栗子,若回调中有长耗时任务,则延时误差会越来越大。解决方案:

js
// 利用 setTimeout,每次执行回调后都对延时进行重新修正
function xsetInterval(callback, delay) {
    let count = 0;
    let startTime = new Date().getTime();

    function fixed() {
        count++;
        let offset = new Date().getTime() - (startTime + count * delay);
        let nextTime = delay - offset;

        callback();
        setTimeout(fixed, nextTime < 0 ? 0 : nextTime);
    }

    return setTimeout(fixed, delay);
}

获取全局下的所有自定义变量/属性

js
(function getWinAttr(){
    let body = document.getElementsByTagName('body')[0];
    let ifr = document.createElement('iframe');
    let ifrWin;

    body.appendChild(ifr);
    ifrWin = ifr.contentWindow;

    for(i in window){
        if(!(i in ifrWin)){
            console.log(i);
        }
    }

    body.removeChild(ifr);
}());

js 获取图片宽高

HTML5 提供了一个新属性 naturalWidth, naturalHeight 可以直接获取图片的原始宽高(图片需已加载)。这两个属性在 Firefox/Chrome/Safari/Opera 及 IE9 里已经实现。改造下获取图片尺寸的方法:

js
/**
 * IE6/7/8 兼容处理:
 * 创建了一个新的 img,仅设置其 src,这时需要让图片完全载入后才可以获取其宽高
 */
function getImgNaturalDimensions(img, callback) {
    let nWidth, nHeight;

    return new Promise((resolve, reject) => {
        if (img.naturalWidth) { // 现代浏览器
            resolve([img.naturalWidth, img.naturalHeight]);
        } else { // IE6/7/8
            let image = new Image();
            image.src = img.src;
            image.onload = function () {
                resolve([image.width, image.height]);
                image.onload = null;
            }
        }
    });
}

前端 Router 简单实现

通过 history 改变 url 有以下几种方法:history.back(), history.forward(), history.go(), history.pushState()history.replaceState()

同时在 history 中还支持一个事件,该事件为 popstate,不过 history.pushState(), history.replaceState() 不会触发 popstate 事件,需要我们手动做拦截处理。

TIP

调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。

js
class Router {
    constructor(options = { mode: 'hash', routes: [] }) {
        this.mode = mode;
        this.routes = routes;

        switch (this.mode) {
            case 'hash':
                window.addEventListener('hashchange', function (evt) {
                    let hash = location.hash.slice(1);
                    let route = this.routes.find(item => item.path.test(hash)) || {};

                    route.func && route.func();
                });
                break;
            case 'history':
                /**
                 * history.pushState, history.replaceState 不会触发
                 * popstate 事件,需做劫持处理
                 */
                function hisRewrite(type) {
                    let origin = history[type];

                    return function () {
                        let result = origin.apply(this, [...arguments]);
                        let e = new Event(type);
                        e.arguments = arguments;
                        window.dispatchEvent(e);

                        return result;
                    }
                }

                function changeCallback(evt) {
                    let pathname = location.pathname;
                    let route = this.routes.find(item => item.path.test(pathname)) || {};

                    route.func && route.func();
                }

                history.pushState = hisRewrite('pushState');
                history.replaceState = hisRewrite('replaceState');

                window.addEventListener('popstate', changeCallback);
                window.addEventListener('pushState', changeCallback);
                window.addEventListener('replaceState', changeCallback);

                break;
            default:
                break;
        }
    }

    push(route) {
        let mode = this.mode;

        switch (mode) {
            case 'hash':
                location.hash = route.name;
                break;
            case 'history':
                history.pushState(route.options, route.title, route.name);
                break;
        }
    }

    replace() {}
}

根据数组项中的某个值进行排序

sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的。

由于它取决于具体实现,因此无法保证排序的时间和空间复杂性。

js
var arr = [
    { name: '张飞', age: 34 },
    { name: '关羽', age: 30 },
    { name: '刘备', age: 50 }
];

/**
 * 字符排序可以用 A.localeCompare(B) 若 A 大于 B 则返回大于 0 的数字,相等返回 0
 */
arr.sort(function (a, b) {
    if (a.name > b.name) {
        return 1;
    } else if (a.name < b.name) {
        return -1;
    }

    return 0;
    // return a.name.localeCompare(b.name);
});

requestAnimationFrame 兼容

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

js
(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] // Webkit 中此取消方法的名字变了
            || window[vendors[x] + 'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
            var id = window.setTimeout(function() {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
    }
    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
    }
}());

求页面元素节点数、嵌套深度、最大子元素个数

DOM 的体积过大会影响页面性能,假如你想在用户关闭页面时统计(计算并反馈给服务器)当前页面元素节点的数量总和、元素节点的最大嵌套深度以及最大子元素个数,请用 JS 配合原生 DOM API 实现需求(不用考虑陈旧浏览器以及在现代浏览器中的兼容性,可以使用任意浏览器的最新特性;不用考虑 shadow DOM)。比如在如下页面中运行后:

html
<html>
    <head></head>
    <body>
        <div>
            <span>f</span>
            <span>o</span>
            <span>o</span>
        </div>
    </body>
</html>

会输出:

json
{
    totalElementsCount: 7,
    maxDOMTreeDepth: 4,
    maxChildrenCount: 3
}

编程实现(可以查阅相关 DOM API,但是不可以使用前端框架 or 类库):

js
export function calculateDOMNodes () {
    // your implementation code here:
}
window.addEventListener('close', calculateDOMNodes);

参考题解

js
let counts;

window.addEventListener('beforeunload', () => {
    counts = walk(document.body);
    navigator.sendBeacon('http://127.0.0.1:10000/beacon', JSON.stringify(counts));
});

window.addEventListener('close', calculateDOMNodes);

function calculateDOMNodes () {
    navigator.sendBeacon('http://127.0.0.1:10000/beacon', JSON.stringify(counts));
}

console.log(walk(document.body));

function walk (root) {
    let start = Date.now();
    let stack = [root];
    let nextLevel = [];

    let totalElementsCount = -1;
    let maxDOMTreeDepth = 0;
    let maxChildrenCount = 0;

    while (stack.length || nextLevel.length) {
        if (!stack.length) {
            stack = nextLevel;
            nextLevel = [];
            maxDOMTreeDepth++;
            continue;
        }
        let ele = stack.pop();
        totalElementsCount++;
        if (maxChildrenCount < ele.children.length) maxChildrenCount = ele.children.length;
        nextLevel.push(...ele.children);
    }

    return {
        totalElementsCount,
        maxDOMTreeDepth,
        maxChildrenCount,
        time: Date.now() - start
    };
}

实现 Ajax 并发请求控制

js
function multiRequest(urls = [], maxNum) {
    // 请求总数量
    const len = urls.length;
    // 根据请求数量创建一个数组来保存请求的结果
    const result = new Array(len).fill(false);
    // 当前完成的数量
    let count = 0;

    return new Promise((resolve, reject) => {
        // 请求 maxNum 个
        while (count < maxNum) {
            next();
        }
        function next() {
            let current = count++;
            // 处理边界条件
            if (current >= len) {
                // 请求全部完成就将 promise 置为成功状态, 然后将 result 作为 promise 值返回
                !result.includes(false) && resolve(result);
                return;
            }
            const url = urls[current];
            console.log(`开始 ${current}`, new Date().toLocaleString());
            fetch(url)
                .then((res) => {
                    // 保存请求结果
                    result[current] = res;
                    console.log(`完成 ${current}`, new Date().toLocaleString());
                    // 请求没有全部完成, 就递归
                    if (current < len) {
                        next();
                    }
                })
                .catch((err) => {
                    console.log(`结束 ${current}`, new Date().toLocaleString());
                    result[current] = err;
                    // 请求没有全部完成, 就递归
                    if (current < len) {
                        next();
                    }
                });
        }
    });
}

实现并发控制

js
const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time);
});

const scheduler = new Scheduler();
const addTask = (time, order) => {
    scheduler.add(() => timeout(time))
        .then(() => console.log(order));
};

// 限制同一时刻只能执行 2 个 task
addTask(4000, '1');
addTask(3500, '2');
addTask(4000, '3');
addTask(3000, '4');

/**
 * 实现 Scheduler 类,使控制台打印顺序为:
 * 3.5 秒打印 2
 * 4 秒后打印 1
 * 7 秒后打印 4
 * 7.5 秒后打印 3
 */

简单实现:

js
class Scheduler {
    constructor(parallels = 2) {
        this.fns = [];
        this.callbacks = [];
        this.count = 0;
        this.parallels = parallels;
    }

    add(fn) {
        const run = () => {
            const fn = this.fns.shift();
            const callback = this.callbacks.shift() || (() => {});

            if (!fn) return;

            fn().then(function (result) {
                callback(result);
                run();
            });
        };

        this.fns.push(fn);
        while (this.count < this.parallels) {
            // run();
            setTimeout(run);
            this.count++;
        }

        return this;
    }

    then(callback) {
        this.callbacks.push(callback);
        return this;
    }
}

如何快速让字符串变成以千为精度的数字

正则 '1234567890'.replace(/\B(?=(\d{3})+(?!\d))/g, ',')

如何将浮点数点左边的数每三位添加一个逗号,如 12000000.11 转化为 12,000,000.11

toLocaleString()

js
function format(number) {
    return number.toLocaleString();
}

replace

js
function format(number) {
    return number && number.replace(/(?!^)(?=(\d{3})+\.)/g, ',');
}
js
/**
 * 支持小数的千位分隔
 *
 * /\d(?=(\d{3})+$)/g
 * /(?!^)(?=(\d{3})+$)/g
 * 以上两个正则也可实现千位分隔需求
 *
 * 解析:
 * 1. \B: 匹配非单词边界,此处可以 `(?!^)` 匹配起始位置,或以 `\d` 匹配单个数字替代
 * 2. (?=(\d{3})+$): 正向肯定预查,在匹配 `$` 结束位置处开始查找,重复出现的` (\d{3})` 3个数字
 * 3. (?=(\d{3})+\.): 正向肯定预查,在匹配 `\.` 小数点处开始查找,重复出现的` (\d{3})` 3个数字
 */
function formatNumber(number) {
    if (!number) return;

    // 整数
    if (Math.floor(number) === +number) {
        return number.toString().replace(/\B(?=(\d{3})+$)/g, ',');
    }

    // 小数
    return number.toString().replace(/\B(?=(\d{3})+\.)/g, ',');
}

手写防抖、节流函数

  • 防抖 (debounce):所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间(常用于 input 输入框或 button 提交等场景)
  • 节流 (throttle):所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数,节流会稀释函数的执行频率(常用于 resize, scroll 触发回调场景)

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于 wait 时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数 wait)调用函数。

js
/**
 * @desc 函数防抖
 * 一定时间内多次触发,会重置延迟时间,且只会执行最后一次调用
 */
function debounce(fn, wait = 300, ctx = this) {
    let timer = null;

    return function (...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(ctx, args);
        }, wait);
    }
}

/**
 * @desc 函数节流
 * 一定时间内多次触发只会执行第一次调用
 */
function throttle(fn, wait = 300, ctx = this) {
    let flag = false;

    return function (...args) {
        if (flag) return;

        flag = true;
        setTimeout(() => {
            flag = false;
            fn.apply(context, args);
        }, wait);
    }
}

/**
 * @desc 函数节流
 * 一定时间内多次触发只会执行第一次调用,且立即触发
 */
function throttle(fn, delay = 300) {
    let timer;
    let flag;

    return function (...args) {
        if (flag) return;

        const ctx = this;

        flag = true;
        fn.apply(ctx, args);
        setTimeout(() => {
            flag = false;
        }, delay);
    }
}

升级版:

js
/**
 * @desc 函数防抖
 * @param {Function} fn 函数
 * @param {Number}   delay 延迟执行毫秒数
 * @param {Boolean}  immediate true 表立即执行,false 表非立即执行
 */
function debounce(fn, delay = 300, immediate) {
    let timeout;

    return function (...args) {
        const context = this;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            const callNow = !timeout;
            timeout = setTimeout(() => {
                timeout = null;
            }, delay);
            if (callNow) fn.apply(context, args);
        } else {
            timeout = setTimeout(() => {
                fn.apply(context, args);
            }, delay);
        }
    };
};

/**
 * @desc 函数节流
 * @param {Function} fn 函数
 * @param {Number}   delay 延迟执行毫秒数
 * @param {Number}   mustRunDelay 超过一定时间内必须执行毫秒数
 */
function throttle(fn, delay = 300, mustRunDelay) {
    let timer = null;
    let tStart;

    return function (...args) {
        const context = this;
        const tCurr = +new Date();

        clearTimeout(timer);

        if (!tStart) {
            tStart = tCurr;
        }

        if (mustRunDelay && tCurr - tStart >= mustRunDelay) {
            fn.apply(context, args);
            tStart = tCurr;
        } else {
            timer = setTimeout(() => {
                fn.apply(context, args);
            }, delay);
        }
    };
};

实现一个 render/template 函数,可以用于模板渲染

js
function template(html, data, reg) {
    reg = reg || /\{\{(\s?[\w.]+\s?)\}\}/g;

    return html.replace(reg, function(m, name) {
        console.log(m, name, name.trim());
        const value = eval(`data.${name.trim()}`);
        if(value !== undefined) {
            var ret ;
            if(value instanceof Function) {
                ret = value.call(data);
            } else {
                ret =  value;
            }

            return reg.test(ret) ? template(ret, data, reg) : ret;
        } else {
            return '';
        }
    });
}

// test
var html = '{{ user.name }},你都 {{ user.age }} 岁了,还不好好学习,天天向上?!';
var data = {
    user: {
        age: 18,
        name: 'jimco',
    },
};

console.log(template(html, data)); // jimco,你都 18 岁了,还不好好学习,天天向上?!

实现一个 JSON.stringify

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

  • Boolean | Number| String 类型会自动转换成对应的原始值
  • undefined、任意函数以及 Symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)
  • 不可枚举的属性会被忽略
  • 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略
js
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 函数用以在返回之前对所得到的对象执行变换(操作)。

js
/**
 * 1. 直接调用 eval 实现
 */
function jsonParse(opt) {
    return eval('(' + opt + ')');
}

/**
 * 2. 利用 Function 实现
 */
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();

参考:JSON.parse 三种实现方式