前端手写题总结(持续更新...)

141 阅读29分钟
  1. juejin.cn/post/694613…
  2. juejin.cn/post/703327…
  3. juejin.cn/post/685512…
  4. juejin.cn/post/684490…
  • promise
  • new
  • ES5的继承
  • 防抖和节流
  • 柯里化
  • bind call apply
  • ajax 和 ajax的封装+promise 封装ajax
  • 深浅拷贝
  • 数组去重 数组扁平 数组对象转树结构
  • sleep函数
  • 发布订阅模式 和观察者模式
  • setTimeout实现setInterval
  • async 和 await
  • 实现一个事件委托
  • div 拖拽
  • 进制转换

js 基础

promise

实现一个同时允许任务数量最大为n的函数

new的实现

在调用 new 的过程中会发生以上四件事情: (1)首先创建了一个新的空对象 (2)设置原型,将对象的原型设置为函数的 prototype 对象。 (3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性) (4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

function myNew () {
    let constructor = Array.prototype.shift.call(arguments)
    if (typeof constructor !== 'function') {
        console.log('type Error')
        return
    }
    // 新建一个对象
    let newObject = null
    let result = null
    // 将新创建的对象的原型设置 为 函数的prototye 对象
    newObject = Object.create(constructor.prototype)
    // 改变 this 指向并执行函数把 结果存起来
    result = constructor.apply(newObject, arguments)
    // 判断构造函数的是否有返回,并判断返回 的 类型
    let flag = result && (typeof result === 'object' || typeof result === 'functioin')
    return flag ? result : newObject
}
// 使用此方法
myNew(构造函数, 初始化参数);

js的继承实现

类式继承/ 原型继承

function SubClass() {
  this.subValue = false;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getSubValue = function() {
  return this.subValue;
}

var instance = new SubClass();

console.log(instance)
console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false

从我们之前介绍的 instanceof 的原理我们知道,第三个 console 如果这么写就返回 true 了console.log(SubClass.prototype instanceof SuperClass)

虽然实现起来清晰简洁,但是这种继承方式有两个缺点:

  • 由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类中如果共有属性是引用类型,就会在子类中被所有的实例所共享,因此一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其他的子类
  • 由于子类实现的继承是靠其原型prototype对父类进行实例化实现的,因此在创建父类的时候,是无法向父类传递参数的。因而在实例化父类的时候也无法对父类构造函数内的属性进行初始

构造继承

function SuperClass(id) {
  this.books = ['js','css'];
  this.id = id;
}
SuperClass.prototype.showBooks = function() {
  console.log(this.books);
}
function SubClass(id) {
  //继承父类
  SuperClass.call(this,id);
}
//创建第一个子类实例
var instance1 = new SubClass(10);
//创建第二个子类实例
var instance2 = new SubClass(11);

instance1.books.push('html');
console.log(instance1)
// {
//     "books": [
//         "js",
//         "css",
//         "html"
//     ],
//     "id": 10
// }
console.log(instance2) 
//  {
//     "books": [
//         "js",
//         "css"
//     ],
//     "id": 11
// }
instance1.showBooks();//TypeError

SuperClass.call(this,id)当然就是构造函数继承的核心语句了.

  • 由于父类中给this绑定属性,因此子类自然也就继承父类的共有属性。
  • 由于这种类型的继承没有涉及到原型prototype,所以父类的原型方法自然不会被子类继承,而如果想被子类继承,就必须放到构造函数中,
  • 这样创建出来的每一个实例都会单独的拥有一份而不能共用,这样就违背了代码复用的原则,所以综合上述两种,我们提出了组合式继承方法

组合式继承

function SuperClass(name) {
  this.name = name; 
  this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
    console.log(this.books);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
SubClass.prototype = new SuperClass();

SubClass.prototype.getTime = function() {
  console.log(this.time);
}

如上,我们就解决了之前说到的一些问题,但是是不是从代码看,还是有些不爽呢?至少这个SuperClass的构造函数执行了两遍就感觉非常的不妥.

原型式继承

function inheritObject (o) {
    // 声明一个过渡对象
    function F () {}
    // 过渡对象的原型继承父对象
    F.prototype = o
    // 返回过渡对象的实例,该对象的原型继承了父对象
    return new F ()
}

原型式继承大致的实现方式如上,是不是想到了我们new关键字模拟的实现?

其实这种方式和类式继承非常的相似,他只是对类式继承的一个封装,其中的过渡对象就相当于类式继承的子类,只不过在原型继承中作为一个普通的过渡对象存在,目的是为了创建要返回的新的实例对象。

var book = {
    name:'js book',
    likeBook:['css Book','html book']
}
var newBook = inheritObject(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = inheritObject(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);

如上代码我们可以看出,原型式继承和类式继承一个样子,对于引用类型的变量,还是存在子类实例共享的情况。

寄生式继承

var book = {
    name:'js book',
    likeBook:['html book','css book']
}
function createBook(obj) {
    //通过原型方式创建新的对象
  var o = new inheritObject(obj);
  // 拓展新对象
  o.getName = function(name) {
    console.log(name)
  }
  // 返回拓展后的新对象
  return o;
}
var newBook = createBook(book);
newBook.name = 'ajax book';
newBook.likeBook.push('react book');
var otherBook = createBook(book);
otherBook.name = 'canvas book';
otherBook.likeBook.push('node book');
console.log(newBook,otherBook);

其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法。

对于引用类型的变量,还是存在子类实例共享的情况。

寄生组合式继承

回到之前的组合式继承,那时候我们将类式继承和构造函数继承组合使用,但是存在的问题就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承

而寄生组合式继承是寄生式继承和构造函数继承的组合。但是这里寄生式继承有些特殊,这里他处理不是对象,而是类的原型。

function inheritObject(o) {
  //声明一个过渡对象
  function F() { }
  //过渡对象的原型继承父对象
  F.prototype = o;
  //返回过渡对象的实例,该对象的原型继承了父对象
  return new F();
}

function inheritPrototype(subClass,superClass) {
    // 复制一份父类的原型副本到变量中
  var p = inheritObject(superClass.prototype);
  // 修正因为重写子类的原型导致子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类原型
  subClass.prototype = p;
}

组合式继承中,通过构造函数继承的属性和方法都是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。 我们需要继承的仅仅是父类的原型,不用去调用父类的构造函数。换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的constructor属性指向的不是subClass子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。

function SuperClass(name) {
  this.name = name;
  this.books=['js book','css book'];
}
SuperClass.prototype.getName = function() {
  console.log(this.name);
}
function SubClass(name,time) {
  SuperClass.call(this,name);
  this.time = time;
}
inheritPrototype(SubClass,SuperClass);
SubClass.prototype.getTime = function() {
  console.log(this.time);
}
var instance1 = new SubClass('React','2017/11/11')
var instance2 = new SubClass('Js','2018/22/33');

instance1.books.push('test book');

console.log(instance1.books,instance2.books);
instance2.getName();
instance2.getTime();

防抖

基础版

function debounce (func, wait) {
    var timeout, result
    return function () {
        var context = this // this 指向
        var args = arguments // 可能有参数
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout (function () {
            result = func.apply(context, args) // 函数是否有返回结果,要返回结果
        }, wait)
    }
}

是否立即执行

这个时候,代码已经很是完善,但是为了让这个函数更加完善,我们接下来思考一个新的需求。 这个需求就是: 我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行

function debounce (func, wait, immediate) {
    var timeout, reuslt
    return function () {
        var context = this
        var args = arguments
        if (timeout) clearTimeout(timeout)
        if (immediate) {
            var callNow = !timeout
            timeout = setTimeout (function () {
                timeout = null
            }, wait)
            if (callNow) result = func.apply(contex, args)
        } else {
            timeout = setTimeout (functioin () {
                result = func.apply(context, args)
            }, wait)
        }
        return result
    }
}

中途取消

最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?

// 第六版
function debounce(func, wait, immediate) {
    var timeout, result;
    var debounced = function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                result = func.apply(context, args)
            }, wait);
        }
        return result;
    };
    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
}

节流

节流的原理很简单: 如果你持续触发事件,每隔一段时间,只执行一次事件。 根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。 关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

时间戳

function throttle (func, wait) {
    var context, args
    var previous = 0
    return function () {
        var now = +new Date()
        context = this
        args = arguments
        if (now - previous > wait) {
            func.apply(context, args)
            previous = now
        }
    }
}

定时器

function throttle (func, wait) {
    var timeout
    var previous = 0
    return function () {
        contetx = this
        args = arguments
        if (!timeout) {
            timeout = setTimeout(function () {
                tiemout = null
                func.apply(context, args)
            }, wait)
        }
    }
}

所以比较两个方法:

第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

优化(这个部分就是扩展)

双剑合璧

有人就说了:我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次!

所以我们综合两者的优势,然后双剑合璧,写一版代码:

function throttle(func, wait) {
    var timeout, context, args, result;
    var previous = 0;

    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };

    var throttled = function() {
        var now = +new Date();
        //下次触发 func 剩余的时间
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
         // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

优化

但是我有时也希望无头有尾,或者有头无尾,这个咋办? 那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定: leading:false 表示禁用第一次执行trailing: false 表示禁用停止触发的回调 我们来改一下代码:

// 第四版
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

增加取消功能

在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:

...
throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}
...

注意

我们要注意 实现中有这样一个问题: 那就是 leading:false 和 trailing: false 不能同时设置。 如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});

函数柯里化

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

用途

先来个例子

// 示意而已
function ajax(type, url, data) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")

// 利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");

// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");
  • curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。 如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 map 呢?
var person = [{name: 'kevin'}, {name: 'daisy'}]
// 如果我们要获取所有的 name 值,我们可以这样做:
var name = person.map(function (item) {
    return item.name;
})

不过如果我们有 curry 函数:

var prop = curry(function (key, obj) {
    return obj[key]
});

var name = person.map(prop('name'))

我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些? 但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了? person.map(prop('name')) 就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。

初级版

var curry = function (fn) {
    var args = [].slice.call(arguments, 1)
    return function () {
        var newArgs = args.concat([].slice.call(arguments))
        return fn.apply(this, newArgs)
    }
}

进阶版


function curry(fn, args) {
    length = fn.length;
    args = args || [];
    return function() {
        var _args = args.slice(0),
            arg, i;
        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            _args.push(arg);
        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        } else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

ES6

// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

占位符的进阶 (仅参考学习)

curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢?

curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢? 我们可以创建一个占位符,比如这样:

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b") // ["a", "b", "c"]

代码实现

// 第三版
function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 处理类似 fn(1)(_) 这种情况
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 处理类似 fn(_, 2)(1) 这种情况
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用参数 1 替换占位符
                else {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var _ = {};

var fn = curry(function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
});

// 验证 输出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)

偏函数(仅参考学习)

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。

什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

实际上你会发现这个和柯里化太像了,所以两者到底是有什么区别呢? 其实也很明显: 柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。 局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

第一版

// 似曾相识的代码
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

写个dom 验证下 this 指向

function add(a, b) {
    return a + b + this.value;
}

// var addOne = add.bind(null, 1);
var addOne = partial(add, 1);

var value = 1;
var obj = {
    value: 2,
    addOne: addOne
}
obj.addOne(2); // ???
// 使用 bind 时,结果为 4
// 使用 partial 时,结果为 5

具体可参考 juejin.cn/post/684490…

函数手写之 call

原理

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

试想当调用 call 的时候,把 foo 对象改造成如下:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

但是这样却给 foo 对象本身添加了一个属性,这可不行呐!

不过也不用担心,我们用 delete 再删除它不就好了~

所以我们模拟的步骤可以分为:

将函数设为对象的属性 执行该函数 删除该函数 以上个例子为例,就是

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

实现

判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 处理传入的参数,截取第一个参数后的所有参数。 将函数作为上下文对象的一个属性。 使用上下文对象来调用这个方法,并保存返回结果。 删除刚才新增的属性。 返回结果。

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
      result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

参考 juejin.cn/post/684490…

函数手写之 apply

apply 的实现和 call基本类似 apply 函数的实现步骤:

判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。 判断传入上下文对象是否存在,如果不存在,则设置为 window 。 将函数作为上下文对象的一个属性。 判断参数值是否传入 使用上下文对象来调用这个方法,并保存返回结果。 删除刚才新增的属性 返回结果

// apply 函数实现
Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

函数手写之bind

首先要注意bind的三个问题

  1. bind 返回一个新的函数
  2. bind 返回新的函数还可以传入参数
  3. bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
Function.prototype.myBind = function (context) {
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var fNOP = function () {};
    var fbound = function () {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
    }
    fNOP.prototype = this.prototype;
    fbound.prototype = new fNOP();
    return fbound;
}

参考 juejin.cn/post/684490…

ajax 封装 + promsie 封装ajax

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

ajax

创建AJAX请求的步骤:

  1. 创建一个 XMLHttpRequest 对象。
  2. 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  4. 当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

promise 封装ajax

function myAjax(url,method,data){
    let promise = new Promise((resolve,reject) => {
        let xhr = new XMLHttpRequest()
        xhr.onreadystatechange = function(){
            if(xhr.readyState === 4){
                if((xhr.status >= 200 && xhr.status < 300)||xhr.status === 304){
                    resolve(xhr.response)
                }else{
                    reject(new Error("error"))
                }
            }
        }
        if(method.toUpperCase() === "GET"){
           let paramslist = [];
           for(key in data){
               paramslist.push(key + "=" + data[key])
           }
           //根据get请求方法对url进行拼接
           let params = paramslist.join("&");
           url = url + "?" + params;
           xhr.open("get",url,false);
           //使用get请求将内容连接在url后面
           xhr.send()

        }
        if(method.toUpperCase() === "POST"){
            xhr.open("post", url, false);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
            xhr.send(data);
            //使用post请求时将内容放在send里面
        }
    })
    return promise
}

深浅拷贝

let arr = [1, 2, 3];
let newArr = arr;
newArr[0] = 100;

console.log(arr);//[100, 2, 3]

这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。

let arr = [1, 2, 3];
let newArr = arr.slice();
newArr[0] = 100;

console.log(arr);//[1, 2, 3]

当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦! 这就是浅拷贝!

但是有这种情况

let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;

console.log(arr);//[ 1, 2, { val: 1000 } ]

它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力,深拷贝就是为了解决这个问题而生的

浅拷贝

1. 手动实现

const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

2. Object.assign

Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。

  • 注意 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。 因为null 和 undefined 不能转化为对象,所以第一个参数不能为null或 undefined,会报错。
let obj = { name: 'sy', age: 18 };
const obj2 = Object.assign({}, obj, {name: 'sss'});
console.log(obj2);//{ name: 'sss', age: 18 }

3. 扩展运算符

使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

4. concat浅拷贝数组

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]

5. slice 等

该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。

let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false

深拷贝

1. JSON.parse(JSON.stringify(obj))

JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,RegExp, Date, Set, Map undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。 而且还要考虑到 循环引用的情况

第一版 深拷贝 解决循环引用问题

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

//创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap
const deepClone = (target, map = new WeakMap()) => { 
  if(map.get(target))  
    return target; 
 
 
  if (isObject(target)) { 
    map.set(target, true); 
    const cloneTarget = Array.isArray(target) ? []: {}; 
    for (let prop in target) { 
      if (target.hasOwnProperty(prop)) { 
          cloneTarget[prop] = deepClone(target[prop],map); 
      } 
    } 
    return cloneTarget; 
  } else { 
    return target; 
  } 
}

拷贝特殊对象

  • set 和 map
const getType = Object.prototype.toString.call(obj);

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return;
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.prototype;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key), deepClone(item));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      target.add(deepClone(item));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}
  • 不可遍历的对象
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {
  // 待会的重点部分
}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

拷贝 函数

提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是 Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要 处理普通函数的情况,箭头函数直接返回它本身就好了。

那么如何来区分两者呢? 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

小 bug

const target = new Boolean(false);
const Ctor = target.constructor;
new Ctor(target); // 结果为 Boolean {true} 而不是 false。

对于这样一个bug,我们可以对 Boolean 拷贝做最简单的修改, 调用valueOf: new target.constructor(target.valueOf())。 但实际上,这种写法是不推荐的。因为在ES6后不推荐使用【new 基本类型()】这 样的语法,所以es6中的新类型 Symbol 是不能直接 new 的,只能通过 new Object(SymbelType)。 因此我们接下来统一一下:

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

完整代码

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

参考链接 juejin.cn/post/684490…

数组扁平 + 数组去重 + 数组对象转树

数组扁平

let ary = [1, [2, [3, [4, 5]]], 6];// -> [1, 2, 3, 4, 5, 6]
let str = JSON.stringify(ary);
  1. 调用ES6中的flat方法
ary = ary.flat(Infinity);
  1. replace + split
ary = str.replace(/(\[|\])/g, '').split(',')
  1. replace + JSON.parse
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
ary = JSON.parse(str);
  1. 普通递归
let result = [];
let fn = function(ary) {
  for(let i = 0; i < ary.length; i++) {
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}
  1. 利用reduce函数迭代
function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))

6:扩展运算符 //只要有一个元素有数组,那么循环继续

while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}

这是一个比较实用而且很容易被问到的问题,欢迎大家交流补充。

数组去重

juejin.cn/post/703327…

filter+indexOf

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

使用reduce

let unique_3 = arr => arr.reduce((pre, cur) => pre.includes(cur) ? pre : [...pre, cur], []);

map键值对

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
  let map = {};
  let res = [];
  for(var i = 0; i < array.length; i++) {
    if(!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1;
      res.push(array[i]);
    }
  }
  return res;
}

使用Object 键值对

function unique_3(array) {
    var obj = {};
    return array.filter(function (item, index, array) {
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

数组对象转树

let input = [
  {
    id: 1,
    val: "学校",
    parentId: null,
  },
  {
    id: 2,
    val: "班级1",
    parentId: 1,
  },
  {
    id: 3,
    val: "班级2",
    parentId: 1,
  },
  {
    id: 4,
    val: "学生1",
    parentId: 2,
  },
  {
    id: 5,
    val: "学生2",
    parentId: 3,
  },
  {
    id: 6,
    val: "学生3",
    parentId: 3,
  },
];

function buildTree(arr, parentId, childrenArray) {
  arr.forEach((item) => {
    if (item.parentId === parentId) {
      item.children = [];
      buildTree(arr, item.id, item.children);
      childrenArray.push(item);
    }
  });
}
function arrayToTree(input, parentId) {
  const array = [];
  buildTree(input, parentId, array);
  return array.length > 0 ? (array.length > 1 ? array : array[0]) : {};
}
const obj = arrayToTree(input, null);
console.log(obj);

sleep函数实现

通过 promise 来实现

function sleep(ms){
var temple=new Promise(
(resolve)=>{
console.log(111);setTimeout(resolve,ms)
});
return temple
}
sleep(500).then(function(){
//console.log(222)
})
//先输出了 111,延迟 500ms 后输出 222

通过 async 封装

function sleep(ms){
return new Promise((resolve)=>setTimeout(resolve,ms));
}
async function test(){
var temple=await sleep(1000);
console.log(1111)
return temple
}
test();
//延迟 1000ms 输出了 1111

通过 generate 来实现

function* sleep(ms){
yield new Promise(function(resolve,reject){
console.log(111);
setTimeout(resolve,ms);
})
}
sleep(500).next().value.then(function(){console.log(2222)})

发布订阅和观察者模式

juejin.cn/post/704604…

观察者模式

// 首先明白定义 观察则模式定义了一种 一对多的关系 多个观察者监听同一个目标对象,当这个目标对象发生变化时会通知所有的 观察者,观察则自动更新

// 所以在这里认为 这是两方的事情 是被观察者 和观察者 之间直接联系无中间者参与,还有就是 被观察者通知观察者的变化

abstract class Student {
    constructor(public teacher: Teacher) {

    }
    // 每个观察者都有一个update方法 用来在被观察对象更新的时候  执行更新或触发方法
    public abstract update(): void
}

class  Xueba extends Student {
    public update(): void {
        if (this.teacher.getState() === 'tiwen') {
            console.log(this.teacher.getState(), 'huidatiwen')
        }
    }
}

class Xuezha extends Student {
    public update (): void {
        if (this.teacher.getState() === 'tiwen') {
            console.log(this.teacher.getState(), 'ditibibi')
        }
    }
}
class Teacher {
    private state: string = 'jiangke'
    private students: Student[] = []
    public askQuestion () {
        this.state = 'laoshitiwen'
        this.notifyAllStudents()
    }
    getState() {
        return this.state
    }
    setState () {
        
    }
    attach (student: Student) {
        this.student.push(student)
    }
    notifyAllStudents () {
        this.students.forEach(student => student.update())
    }
}

let teacher = new Teacher ()
teacher.attach(new Xueba(teacher))
teacher.attach(new Xuezha(teacher))
teacher.askQuestion()  // 老师发生事件 状态变化 通知学生 (观察者)

// 在上边的例子中 老师中有 学生的数组, 学生中也有老师的实例 所以这里是关键

// 观察者常用的应用场景

  • // DOM事件绑定
  • // Promise
  • // callback
  • // 生命周期函数
  • // EvetBus
  • // Vue2响应式原理
  • // redux

发布订阅者模式

  • // 订阅者 把自己想 订阅的事情注册到 调度中心
  • // 发布者 把要发布的事件 发布到 调度中心
  • // 然后, 调度中心统一调度 订阅者注册到调度中心的代码

// 虽然 以上两种模式都有发布者和订阅者 ,但是观察者模式中调用是被观察者发布 而发布订阅者是有调度中心的 , 所以观察者模式 是发布和订阅者之间是相互知道的 但是发布订阅模式中是不知道的

// 下面的例子以 租房者 中介和 房东为例子

// 中介
class Agency {
    _topic = {} // envents

    subscirbe(type, listener) {
        let listeners = this._topic[type]
        if (listener) {
            listeners.push(listener)
        } else {
            this._topic[type] = [listener]
        }
    }
    publish (type, ...args) {
        let listeners = this._topic[type] || []
        listeners.forEach(listener => listener(...args))
    }
}

// 房东
class LandLord {
    constructor (public agency: Agency) {

    }
    // type 房子类型  area面积  money 房租价格
    lend(type, area, money) {
        this.agency.publish(type, area, money)
    }
}

// 房客
class Tenant {
    constructor (public angency: Angency, public name: string) {
        
    }
    order (type) {
        this.angency.subscirbe(type, (area, money) => {
            console.log(this.name, area + money)
        })
    }
}
// 搞一个中介
let angency = new Angency()
// 搞两个租客 
let rich = new Tenant(angency, 'rich')
let beipiao = new Tenant(angency, 'beipiao')
rich.order('haozhai')
beipiao.order('danjian')
// 搞一个房东
let landLord = new LandLord()
landLord.lend('haozhai', 1000, 100000)
landLord.lend('danjian', 10 ,2000)

async await

juejin.cn/post/700703… juejin.cn/post/704629…

事件委托

看错误版,(容易过的,看「面试官水平了」)

ul.addEventListener('click', function (e) {
            console.log(e,e.target)
            if (e.target.tagName.toLowerCase() === 'li') {
                console.log('打印')  // 模拟fn
            }
})

有个小bug,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对

<ul id="xxx">下面的内容是子元素1
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素2
        <li>li内容>>> <span> 这是span内容123</span></li>
        下面的内容是子元素3
        <li>li内容>>> <span> 这是span内容123</span></li>
</ul>

这样子的场景就是不对的,那我们看看高级版本


function delegate(element, eventType, selector, fn) {
    element.addEventListener(eventType, e => {
        let el = e.target
        while (!el.matches(selector)) {
            if (element === el) {
                el = null
                break
            }
            el = el.parentNode
        }
        el && fn.call(el, e, el)
    },true)
    return element
}

div 拖拽

var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]
})


document.addEventListener('mousemove', function(e){
  if(dragging === false) return null
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = y - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})

进制转换

function Conver(number, base = 2) {
  let rem, res = '', digits = '0123456789ABCDEF', stack = [];

  while (number) {
    rem = number % base;
    stack.push(rem);

    number = Math.floor(number / base);
  }

  while (stack.length) {
    res += digits[stack.pop()].toString();
  }
  
  return res;
}

set Timeout实现 setInterval

setInterval的缺点 1、无视代码错误 setInterval执行过程中会无视自己调用的代码,会持续不断地调用改代码; 2、无视网络延迟 无视对网络请求的响应是否完成,会不断发送请求; 3、不保证执行 到了时间间隔,如果setInterval需要调用的函数需要花费时间较长,可能就会被直接忽略。

简单版本

setInterval 需要不停循环调用,这让我们想到了递归调用自身:

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => {
      fn() // 递归调用自己
    }, time)
  }
  setTimeout(fn, time)
}

clearInterval

clearInterval 的存在(不然你怎么停下 interval 呢)。 clearInterval 的用法是 clearInterval(id)。而这个 id 是 setInterval的返回值,通过这个 id 值就能够清除指定的定时器。

mySetInterval的返回值

回到我们简单版本的 mySetInterval:

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => {
      fn() // 递归调用自己
    }, time)
  }
  setTimeout(fn, time)
}

现在它的返回值因为没有显示指定,所以是 undefined。因此第一步,我们先要返回一个 id 出去。 那么直接 return setTimeout(fn, time) 可以吗?因为我们知道 setTimeout 也会返回一个id,那么初步构想就是通过 setTimeout 返回的 id,然后调用 clearTimeout(id) 来实现我们的 myClearInterval。 如下:

const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    setTimeout(() => { // 第二个、第三个...
      fn() // 递归调用自己
    }, time)
  }
  return setTimeout(fn, time) // 第一个setTimeout
}

const id = mySetInterval(() => {
  console.log(new Date())
}, 1000)

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(id)
}, 2000)

这显然是不行的。因为 mySetInterval 返回的 id 是第一个 setTimeout 的 id,然而2秒后,要 clearTimeout 时,递归执行的第二个、第三个 setTimeout 等等的 id 已经不再是第一个 id 了。因此此时无法清除。 所以我们需要每次执行 setTimeout的时候把新的 id 存下来。怎么存?我们应该会想到用闭包:

const mySetInterval = (cb, time) => {
  let timeId
  const fn = () => {
    cb() // 执行传入的回调函数
    timeId = setTimeout(() => { // 闭包更新timeId
      fn() // 递归调用自己
    }, time)
  }
  timeId = setTimeout(fn, time) // 第一个setTimeout
  return timeId
}

很不错,到这步我们已经能够将 timeId 进行更新了。不过还有问题,那就是执行 mySetInterval 的时候返回的 id 依然不是最新的 timeId。因为 timeId 只在 fn 内部被更新了,在外部并不知道它的更新。那有什么办法让 timeId 的更新也让外部知道呢? 有的,答案就是用全局变量。

let timeId // 全局变量
const mySetInterval = (cb, time) => {
  const fn = () => {
    cb() // 执行传入的回调函数
    timeId = setTimeout(() => { // 闭包更新timeId
      fn() // 递归调用自己
    }, time)
  }
  timeId = setTimeout(fn, time) // 第一个setTimeout
  return timeId
}

但是这样有个问题,由于 timeId 是Number类型,当我们这样使用的时候:

const id = mySetInterval(() => { // 此处id是Number类型,是值的拷贝而不是引用
  console.log(new Date())
}, 1000)

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(id)
}, 2000)

由于 id 是 Number 类型,我们拿到的是全局变量 timeId 的值拷贝而不是引用,所以上面那段代码依然无效。不过我们已经可以通过全局变量 timeId 来清除计时器了:

setTimeout(() => { // 2秒后清除定时器
  clearTimeout(timeId) // 全局变量 timeId
}, 2000)

但是上面的实现,不仅与我们平时使用的 clearInterval 的用法有所出入,并且由于 timeId 是一个 Number 类型的变量,导致同一时刻全局只能有一个 mySetInterval 的 id 存在,也即无法做到清除多个 mySetInterval 的计时器。 所以我们需要一种类型,既能支持多个 timeId 存在,又能实现 mySetInterval 返回的 id 能够被我们的 myClearInterval 使用。你应该能想到,我们要用一个全局的 Object 来做。 修改代码如下:

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
}

我们的 mySetInterval 依然返回了一个 id 值。只不过这个 id 值是全局变量 timeMap 里的一个键的内容。 我们每次更新 setTimeout 的 id 并不是去更新 timeId,相应的,我们去更新 timeMap[timeId] 里的值。

这样实现后,我们调用 mySetInterval 虽然获取到的 timeId 是不变的,但是我们通过 timeMap[timeId] 获取到的真正的 setTimeout 的 id 值是会一直更新的。 另外为了保证 timeId 的唯一性,在这里我简单用了一个自增的全局变量 id 来保证唯一。 好了,id 值有了,剩下的就是 myClearInterval 的实现了。

myClearInterval实现

const myClearInterval = (id) => {
  clearTimeout(timeMap[id]) // 通过timeMap[id]获取真正的id
  delete timeMap[id]
}

自己实现 setTimeout 和 setInterval

初步实现 setTimeout的实现

function setTimeout_(dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 返回一个promise对象
  return new Promise(reslove => {
    // 操作
    function handle() {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= dalay) {
        // 成功回调
        reslove()
      } else {
        // 递归
        handle()
      }
    }
    // 初次调用
    handle()
  })
}

setTimeout_(10).then(() => {
  alert(10)
})

上面的代码看似没有毛病,但是运行后发现,setTimeout_()里面的值设置小一点没有问题(比如2、3),但是一旦超过,就会造成堆栈溢出,乃至报错。 解决堆栈溢出方法 下面隆重介绍,蹦床函数(trampoline)

蹦床函数

蹦床函数(trampoline)就是将 递归执行 转为 循环执行。 执行的都是同样的步骤,只是反复执行,就好像在蹦床,跳上去,掉下来,在跳上去…

蹦床函数的实现:

function trampoline(f){
  while(f && f instanceof Function && falg){
   f = f()
  }
  return f
}

它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题

  • 最终实现
// 定时器
function setTimeout_(dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 返回一个 Promise 对象
  return new Promise(reslove => {
    // 具体操作
    function handle() {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= dalay) {
       // 成功回调
        reslove()
      } else {
       // 不满足条件继续调用
        return handle
      }
    }
    // 调用蹦床函数、将递归变为循环
    trampoline(handle)()
  })
}

// 蹦床函数
function trampoline(f){
  while(f && f instanceof Function){
    f = f()
  }
  return f
}

setTimeout_(1000).then(res => {
  alert(1000)
})

以上的代码,就能实现效果了 思路:定义一个函数,参数为延迟时间,调用时记录一个第一次时间戳,然后里面返回一个Promise对象,再里面有一个闭包,是执行递归操作的函数,这个函数里面做的事就是记录每一次的时间戳,然后减去第一次的时间戳,得出的就是间隔时间,跟规定的间隔时间作比较,如果大于的话,就调用Promise成功回调。再下面就是将递归转为循环,防止堆栈溢出。最后调用

setInterval的实现

这个跟setTimeout差不多,区别就是这个需要每隔一段时间执行代码,并且需要手动清除

// 如果 falg 为 false就不会继续执行循环操作
let falg = true
// 蹦床函数技术,利用循环
function trampoline(f){
  while(f && f instanceof Function && falg){
    f = f()
  }
  return f
}

// 计时器
function setInterval_(f, dalay) {
  // 第一次的时间戳
  let timestampFirst = Date.now()
  // 操作
  function handle() {
    // 每一次的时间戳
    const timestamp = Date.now()
    if ((timestamp - timestampFirst) >= dalay) {
      // 间隔时间到了就重置第一次时间戳
      timestampFirst = Date.now()
      // 调用函数
      f()
    }
    return handle
  }
  trampoline(handle)()
}

let count = 0
// 调用
setInterval_(function() {
  count ++
  if (count === 3) {
    falg = false
  }
  console.log(count)
}, 1000)

上面这个代码我定义的是在控制台输入1、2、3,然后关闭 思路:同样是判断时间戳,但是跟setTimeout不一样的是每次执行里面的函数需要重置时间,达到每次执行的效果。并且在蹦床函数里面的while增加一个判断,用来控制计时器的停止。 总结:这种东西了解一下,以后当个吹牛逼资本就可以了,毕竟这性能嘛.....

使用requestAnimationFrame实现

requestAnimationFrame基本介绍

实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。 优势:由系统决定回调函数的执行时机。60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿。

用requestAnimationFrame实现setTimeout

function setTimeout_(callback, dalay) {
  // 第一次的时间戳
  const timestampFirst = Date.now()
  // 操作
  function handel() {
    // 每一次的时间戳
    const timestamp = Date.now()
    // 当时间戳减去后大于延迟时间
    if ((timestamp - timestampFirst) >= dalay) {
      callback()
    } else {
      requestAnimationFrame(handel)
    }
  }
  // 初次调用
  requestAnimationFrame(handel)
}

// 使用
setTimeout_(function() {
  console.log(2000)
}, 2000)

上面的代码没啥好说的,就是延续之前的思路,把递归换成requestAnimationFrame 用requestAnimationFrame实现setInterval

class setInterval_ {
  constructor(dalay) {
    // 延迟时间
    this.dalay = dalay || 1000
    // requestAnimationFrame 的ID
    this.id = undefined
    // 控制是否结束的状态
    this.isEnd = false
  }

  // 行动
  action(callback) {
    // 第一次的时间戳
    let timestampFirst = Date.now()
    // 具体操作
    const handel = () => {
      // 每一次的时间戳
      const timestamp = Date.now()
      // 当时间戳减去后大于延迟时间
      if ((timestamp - timestampFirst) >= this.dalay) {
        // 执行回调
        callback()
        // 每次把时间初始化
        timestampFirst = Date.now()
      }
      // 如果状态为 true 不会往下执行
      if (this.isEnd) return
      // 每次调用
      this.id = requestAnimationFrame(handel)
    }
    // 初次调用
    this.id = requestAnimationFrame(handel)
  }

  // 清除
  clearInterval_() {
    // 调用清除requestAnimationFrame的方法
    cancelAnimationFrame(this.id)
    // 改状态
    this.isEnd = true
  }
}

// 使用
const s = new setInterval_(1000)
let count = 0
s.action(() => {
  count ++
  console.log(count)
  if (count >= 3) s.clearInterval_()
})

这个稍微比上面的复杂一点,因为要考虑清除的问题,所以我决定写到类里面,实现思路也跟之前递归的差不多,就是加了个清除cancelAnimationFrame的方法

正则

juejin.cn/post/711924…