566 手写37个 原生JavaScript 系列汇总(含promise A+)

1、promise

// 考虑到兼容性问题,不使用ES6+
; (function () {
  // 构造函数constructor
  function MyPromise(executor) {
    // 参数合法校验
    if (typeof executor !== "function") {
      throw new TypeError('MyPromise resolver ' + executor + ' is not a function');
    }

    // 设置实例的私有属性
    var self = this;
    this.PromiseStatus = 'pending';
    this.PromiseValue = undefined;
    // 【1、实例的resolveFunc函数、rejectFunc函数,为了拿到then中的2个函数,把这2个函数挂载到实例上; 2、值设置为空函数的好处,就是支持resolve方法中不写then。】
    this.resolveFunc = function () { };
    this.rejectFunc = function () { };

    // 修改实例的状态和value:只有当前状态为pending才能修改状态
    function change(status, value) {
      // 下面的self.PromiseStatus不能用传递进来的status
      if (self.PromiseStatus !== "pending") return;
      self.PromiseStatus = status;
      self.PromiseValue = value;
      // 通知基于.then注入的某个方法执行(执行resolve、reject都是异步的) 
      // 【用等待时间为0的setTimeout模拟微任务】
      var delayTimer = setTimeout(function () {
        clearTimeout(delayTimer);
        delayTimer = null;
        // 不用重新定义变量,直接用上面的self.PromiseStatus、self.PromiseValue即可
        var status = self.PromiseStatus;
        var value = self.PromiseValue;
        // 把.then注入的某个方法执行拿出来执行
        status === "fulfilled" ?
          self.resolveFunc.call(self, value) :
          self.rejectFunc.call(self, value);
      }, 0);
    }

    // new MyPromise的时候会给 resolve函数传参value
    function resolve(value) {
      change('fulfilled', value);
    }

    // new MyPromise的时候会给 reject函数传参reason
    function reject(reason) {
      change('rejected', reason);
    }

    // new MyPromise的时候会立即把executor函数执行,executor函数中的第一、二个参数分别为resolve、reject函数
    // executor函数执行出现错误,也会把实例的状态改为失败,且value是失败的原因
    try {
      // 【执行executor,就会根据情况执行resolve、reject中的一个,然后去执行change,再然后决定执行resolveFunc、rejectFunc中的一个】
      executor(resolve, reject);
    } catch (err) {
      change('rejected', err.message);
    }
  }

  // 把MyPromise当作对象
  MyPromise.resolve = function (value) {
    // 创建一个状态为成功的实例,通知这个实例的then中的某个方法执行。如果没有写then,定时器到达一定时间之后,就会去执行成功或失败的方法,但是实例没有成功和失败的方法。
    return new MyPromise(function (resolve) {
      resolve(value);
    });
  };

  MyPromise.reject = function (reason) {
    // function的形参可以下划线占位,不能用null
    return new MyPromise(function (_, reject) {
      reject(reason);
    });
  };

  // MyPromise.prototype
  // 不仅要把resolveFunc、rejectFunc挂载到 实例上,还要知道resolveFunc、rejectFunc执行的时候,是否报错,以及返回值是什么;最后还要返回一个新的promise实例
  MyPromise.prototype.then = function (resolveFunc, rejectFunc) {
    // 参数不传默认值的处理:目的是实现状态的顺延 【不传,或者传的不是函数】
    if (typeof resolveFunc !== "function") {
      // (1) 形参value从哪来?promise实例中执行resolve()传递的数据;加一个函数,这个函数可以接收到value; (2) 不是this.resolveFunc
      resolveFunc = function (value) {
        // 【不传,或者传的不是函数时】怎么往下顺延呢?返回一个resolve即可
        return MyPromise.resolve(value);
      };
    }

    if (typeof rejectFunc !== "function") {
      rejectFunc = function (reason) {
        return MyPromise.reject(reason);
      };
    }

    var self = this;
    // resolveFunc、rejectFunc执行的成功、失败直接影响了新返回的MyPromise的成功、失败
    // 返回的新实例的成功和失败由resolveFunc、rejectFunc执行是否报错来决定,或者由返回值是否为新的MyPromise实例来决定
    return new MyPromise(function (resolve, reject) {
      // 最终目的是执行resolveFunc,外面包一层匿名函数,是想知道resolveFunc执行时,是否报错,返回值是什么
      // document.body.onclick = fn,如果想 改变fn的值、this、预先传参等等,就先绑定一个匿名函数,document.body.onclick = function() { fn() }
      // value 要写在外层的匿名函数中
      self.resolveFunc = function (value) {
        // 这里面的this就是实例了,因为执行change方法,传过来的就是实例,也可以用this写
        try {
          // 1、用不用call改变this都可以;2、then的2个回调函数参数有返回值; 3、value是resolveFunc接收【实例】的返回结果
          var x = resolveFunc.call(self, value);
          // 1、不是promise实例,一定是成功的,执行resolve;2、如果x是promise实例,then会通知resolve或reject执行,通知resolve执行,返回的new MyPromise就是成功的,通知reject执行,返回的new MyPromise就是失败的。如果返回的新实例x是失败的,就执行reject,new MyPromise就是失败的,反之是成功的。
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
        } catch (err) {
          reject(err.message);
        }
      };

      self.rejectFunc = function (reason) {
        try {
          var x = rejectFunc.call(self, reason);
          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
        } catch (err) {
          reject(err.message);
        }
      };
    });
  };

  MyPromise.prototype.catch = function (rejectFunc) {
    return this.then(null, rejectFunc);
  };

  MyPromise.all = function (promiseArr) {
    return new MyPromise(function (resolve, reject) {
      var index = 0; // 成功的个数
      var values = [];

      for (var i = 0; i < promiseArr.length; i++) {
        // 利用闭包的方式保存循环的每一项索引
        (function (i) {
          var item = promiseArr[i];
          // 如果当前项不是Promise,直接算作当前项成功
          !(item instanceof MyPromise) ? item = MyPromise.resolve(item) : null;
          // 回调函数的参数 value、reason来自于 race中的参数 promise实例的promiseValue
          item.then(function (value) {
            index++;
            // 不是values[i] = item,item是promise实例,value才是实例的值
            values[i] = value;
            if (index >= promiseArr.length) {
              resolve(values); // 所有的实例都是成功的
            }
          }).catch(function (reason) {
            reject(reason); // 只要有一个失败,整体就是失败的
          });
        })(i);
      }
    });
  };

  // 补充race
  MyPromise.race = function (promises) {
    return new MyPromise(function (resolve, reject) {
      promises.forEach(function (p) {
        !(p instanceof MyPromise) ? p = MyPromise.resolve(p) : null
        // 回调函数的参数 value、reason来自于 race中的参数 promise实例的promiseValue
        p.then(function (value) {
          resolve(value)
        }).catch(function (reason) {
          reject(reason)
        })
      })
    })
  }

  // 补充finally
  Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    // 不管成功、失败,都会执行callback,放到this.then中,成功执行P.resolve,并在P.resolve中执行callback;失败同理。
    return this.then(
      value => P.resolve(callback()).then(() => value),
      reason => P.resolve(callback()).then(() => { throw reason })
    );
  };

  window.MyPromise = MyPromise;
})();


// ------------------------------------

function fn1() {
  return MyPromise.resolve(1);
}

function fn2() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      resolve(2);
    }, 2000);
  });
}

function fn3() {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      reject(3);
    }, 1000);
  });
}

MyPromise.all([fn1(), fn2(), fn3(), 10])
  .then(function (values) {
    console.log('OK', values);
  })
  .catch(function (reason) {
    console.log('NO', reason);
  });


new MyPromise(function (resolve, reject) {
  // resolve(10);
  reject(20);
}).then(function (value) {
  console.log('OK', value);
  return MyPromise.reject(200);
},
  /* function (reason) {
  console.log('NO', reason);
  return 100;
} */
).then(function (value) {
  console.log('OK', value);
}, function (reason) {
  console.log('NO', reason);
});

2、防抖

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button id="btn">按钮</button>
</body>

</html>
<script>
  function debounce(func, wait = 500, immediate = false) {
    let timer = null;
    return function anonymous(...params) {
      // 第一次触发 或 立即执行的条件:immediate为true,timer为null
      let now = immediate && !timer;
      clearTimeout(timer);
      timer = setTimeout(() => {
        timer = null;
        !immediate ? func.call(this, ...params) : null;
      }, wait);

      // 用于第一次触发 或 立即执行 
      now ? func.call(this, ...params) : null;
    };
  }

  function func() {
    console.log('OK');
  }

  let btn = document.querySelector('#btn')
  btn.onclick = debounce(func, 1000, false)
</script>

3、节流

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button id="btn">按钮</button>
</body>

</html>
<script>
  /*
   * 假设我们频率设置为500ms,我们频繁触发了10000ms,对于防抖,则只触发一次;对于节流,则触发20次 */
  // 【第一次触发时,remaining是负数,previous被赋值为当前时间。】
  // 【第二次触发,假设是在间隔20ms,则remaining = 500 - (新的当前时间 - 上一次触发时间) = 500 - 20 = 480,也就是定时器的等待时间remaining。】
  function throttle(func, wait = 500) {
    let timer = null;
    let previous = 0; // 记录上一次操作时间

    return function anonymous(...params) {
      let now = new Date(); // 当前操作的时间
      let remaining = wait - (now - previous); // 剩余时间
      // remaining <= 0 与 remaining > 0 的区别:后者多了个定时器,previous不一样
      if (remaining <= 0) {
        // 两次间隔时间超过频率:把方法执行即可
        // clearTimeout是从系统中清楚定时器,timer值不会变为null 【银行系统清理排队号】
        clearTimeout(timer);
        // 这是给变量timer赋值为null,就可以通过timer是否为null,判断有木有定时器 【自己把小纸条丢垃圾篓,也可以不扔,拿手里,但是就不能通过timer是否为null,判断有木有定时器了。】
        timer = null;
        previous = now; // 【把上一次操作时间修改为当前时间】
        func.call(this, ...params);
      } else if (!timer) {
        // 两次间隔时间小于频率,如果没有定时器,设置定时器;有定时器了,就不用重新设置定时器,而是以上一次的计时为准
        // 两次间隔时间没有超过频率,说明还没有达到触发标准,设置定时器等待即可(还差多久,就等多久) 【假设时间间隔是500ms,第20ms点击,剩余480ms,就等待480ms。】
        timer = setTimeout(() => {
          // 这两行代码要写到定时器里
          clearTimeout(timer);
          timer = null;
          // 过了remaining时间后,才去执行func,所以previous不能等于now
          previous = new Date(); // 【把上一次操作时间修改为当前时间】
          func.call(this, ...params);
        }, remaining);
      }
    };
  }

  function func() {
    console.log('OK');
  }

  let btn = document.querySelector('#btn')
  btn.onclick = throttle(func, 1000);


  // -------------------------------------------


  //  防止频繁点击触发:设置标识进行判断 
  // let isClick = false;
  // btn.onclick = function () {
  //   // isClick为false,则不继续往下执行
  //   if (isClick) return;
  //   isClick = true;
  //   setTimeout(() => {
  //     console.log('OK');
  //     isClick = false;
  //   }, 500);
  // };
</script>

4、手写实现call

/* call的作用:改变函数中this指向的 */
Function.prototype.call = function call(context, ...params) {
  // this:就是调用call的函数, context:就是第一个参数, params:就是[形参集合]

  // (1)undefined == null 是true;(2)条件成立,context就是window;条件不成立,context就是传进来的值;(3)这里就是处理形参context的值为ndefined、null的情况。
  context == null ? context = window : null;
  // 只有引用数据类型值才能设置对应的属性
  let contextType = typeof context;
  if (!/^(object|function)$/.test(contextType)) {
    // 不是引用类型我们需要把其变为引用类型
    // 下面几行代码,直接一行即可:context = Object(context)
    if (/^(symbol|bigint)$/.test(contextType)) {
      // symbol、bigint:基于Object创建对象值
      context = Object(context);
    } else {
      // 其余的可以基于new它的构造函数创建 
      // 【数值、字符串、布尔也可以用Object()转为对象:Object(11):Object(11); Object('aa'):String {"aa"};Object(true):Boolean {true}】
      context = new context.constructor(context);
    }
  }
  // 设置一个唯一的属性名 
  let key = Symbol('key'),
    result;
  // 给当前设置属性, 属性值是要执行的函数
  context[key] = this;
  // 让函数执行, 此时函数中的this => context 【context[key]:成员访问,this指向context。】
  result = context[key](...params);
  delete context[key]; // 用完移除
  return result;
};

let obj = {
  name: '哈哈'
};

function func(x, y) {
  console.log(this, x + y);
}


// obj.func(); // => Uncaught TypeError: obj.func is not a function
// 自己处理:obj.xxx=func  只要让obj.xxx执行,也就相当于把func执行,但是此时方法中的this一定是obj了

//  => func基于原型链查找机制,找到Function.prototype.call方法,把call方法执行
//  => 在call方法内部执行中,才是把func执行,并且让里面的this变为obj,并且把10、20传递给func
func.call('xxx', 10, 20);
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') throw new TypeError('Error')
  // 完善部分,如果传入context是个基础类型是无法绑定fn函数的,所以

  if (typeof context === 'object') {
    context = context || window;
  } else {
    context = Object.create(null)
  }

  context = context || window
  // 如果context中有fn则会被覆盖并清除
  // newContext.fn = this
  // 使用Symbol()独一无二数据类型避免fn冲突
  let fn = Symbol('fn')
  context[fn] = this
  let args
  let result
  if ([...arguments][1]) {
    args = [...arguments].slice(1)
    result = newContext.fn(args)
  } else {
    result = newContext.fn()
  }
  delete context[fn]
  return result
}

function fn() {
  console.log(this.a, this)
}

const obj = {
  a: 21
}

fn.myCall(obj)

更简便的手写call

Function.prototype.myCall = function (context, ...args) {
  context = context || window
  const symbol = Symbol()
  context[symbol] = this
  const result = context[symbol](...args)
  delete context[symbol]
  return result
}

5、手写实现一个new方法

// 有些简单函数就不用再画开辟堆保存函数的图
// Object.create处理兼容
Object.create = function create(prototype) {
  function Fn() { }; // 【创建一个空对象】
  Fn.prototype = prototype; // 【让空对象的prototype指向prototype】
  Fn.prototype.constructor = Fn // 【加的】
  return new Fn; // 【返回一个实例对象】

  // 也可以下面这样写,但是不推荐用__proto__
  // Fn.__proto__ = prototype
  // return Fn
};

function _new(Func, ...args) {
  // 【把Func.prototype作为新对象obj的原型,即obj.__proto__ = Func.prototype】
  let obj = Object.create(Func.prototype);
  // 【执行Func,让Func中的this指向obj。】
  let result = Func.call(obj, ...args);
  // 【不是引用类型值,返回obj。undefined == null。】
  if (result == null || !/^(object|function)$/.test(typeof result)) return obj;
  return result;
}

function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function () {
  console.log('wangwang');
};

Dog.prototype.sayName = function () {
  console.log('my name is ' + this.name);
};

let sanmao = _new(Dog, '三毛');
sanmao.bark(); // "wangwang"
sanmao.sayName(); // "my name is 三毛"
console.log(sanmao instanceof Dog); // => true
function _new(fn, ...arg) {
  let obj = {}
  let con = [].slice.call(arguments)
  obj.__proto__ = con.prototype //链接原型
  const ret = fn.call(obj, ...arg); //改变this的指向
  return ret instanceof Object ? ret : obj;
}

6、instanceof

// example:要检测的实例 
// classFunc:要检测的类
function instance_of(example, classFunc) {
  // TZH: 判断是否是基本类型要反向判断,即判断是否object(null特殊处理)和function。
  // 因为纵观es发展历史,基本类型还可能会继续增加,但是引用类型应该会一直只有刚才说那两个,因为es进化时会考虑网络兼容性。
  // 判断是否function、object可以保证随着es进化继续向后兼容。
  // 下面2行是我增加的代码
  if (example === null) return false
  if (!/^object|function$/.test(typeof example)) return false

  // 找当前实例的原型,相当于example.__proto
  let proto = Object.getPrototypeOf(example);
  let classPrototype = classFunc.prototype;
  while (true) {
    // 到了Object.prototype.__proto__
    if (proto === null) return false;
    // 在当前实例的原型链上找到了当前类
    if (proto === classPrototype) return true;
    // 继续找上一级的原型 【不是找classPrototype的上一级原型链。】
    proto = Object.getPrototypeOf(proto);
  }
}

// Function.prototype : Symbol.hasInstance
console.log([] instanceof Array);
// 浏览器内部其实是基于Symbol.hasInstance检测的
console.log(Array[Symbol.hasInstance]([]));

let res = instance_of([12, 23], Array);
console.log(res); // true

let res2 = instance_of({}, Object)
console.log(res2) // true

let res3 = instance_of(function () { }, Object)
console.log(res3) // true

let res4 = instance_of({}, Array)
console.log(res4) // false

let res5 = instance_of(function () { }, Array)
console.log(res5) // false

let res6 = instance_of(11, Number)
console.log(res6) // false


7、数组去重

方法一:利用Set

const res1 = Array.from(new Set(arr));
const res2 = [...new Set(arr)];


方法二:两层for循环+splice

const unique1 = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能
        len--;
        j--;
      }
    }
  }
  return arr;
}


方法三:利用indexOf

const unique2 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}

当然也可以用include、filter,思路大同小异。


方法四:利用includes

const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}


方法五:利用filter

const unique4 = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}


方法六:利用Map

const unique5 = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true)
      res.push(arr[i]);
    }
  }
  return res;
}


老周版

let obj = { aa: 11, bb: 22 }
console.log(obj.cc) // undefined

~ function () {
  /*
   * myUnique : 实现数组去重
   *   @params
   *   @return
   *      [Array] 去重后的数组
   * by 666 on 20190805
   */
  function myUnique() {
    // 此时没有传递要操作的ARY进来,但是方法中的THIS是当前要操作的数组,因为是该数组调用该方法:ARY.MYUNIQUE()
    let obj = {};
    for (let i = 0; i < this.length; i++) {
      let item = this[i];
      // 如果obj中没有item这一项,就是undefined;不等于undefined,说明有了
      if (typeof obj[item] !== 'undefined') {
        // (1)删除重复项,会把这一项后面的所有项都往前提一位,性能差;
        // (2)下一轮循环,i++,就会空出一位,防止出现塌陷问题,i--;
        // (3)最后一项拿过来,替换当前项,当前项就不能用了,然后把最后一项删除
        this[i] = this[this.length - 1];
        this.length--;
        i--;
        continue; // 存在了,就不往里存了
      }
      obj[item] = item;
    }
    obj = null;
    // 保证当前方法执行完返回的结果依然是ARRAY类的一个实例
    return this;
  }
  // => 扩展到内置类的原型上
  Array.prototype.myUnique = myUnique;
}();

let ary = [12, 23, 13, 12, 23, 24, 34, 13, 23];
// ary.myUnique(); 返回去重后的数组(也是ARRAY类的实例)
// ary.sort((a, b)  =>  a - b); 返回排序后的数组
// => 链式写法(保证返回值依然是当前类的实例 一般都会RETURN THIS)
// ary.myUnique().sort((a, b)  =>  a - b).reverse().slice(2).push('珠峰').concat(12);// => Uncaught TypeError: ary.myUnique(...).sort(...).reverse(...).slice(...).push(...).concat is not a function  执行完push返回的是一个数字(新增后数组的长度),不是数组了,不能再继续使用数组的方法
ary.myUnique().sort((a, b) => a - b).reverse();
console.log(ary);


/* Array.prototype.push = function () {
  console.log("哈哈哈");
}
let ary = [1, 2, 3];
ary.push(100); // => "哈哈哈"
console.log(ary); // => 数组没变*/


8、用ES5实现数组的map方法

  • 特点:
  1. 循环遍历数组,并返回一个新数组
  2. 回调函数一共接收3个参数,分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」
  • 用法:
let array = [1, 2, 3].map((item) => {
  return item * 2;
});

console.log(array);  // [2, 4, 6]

  • 实现:
Array.prototype.map = function(fn) {
  let arr = [];
  for(let i = 0; i < this.length; i++) {
    arr.push(fn(this[i], i, this));
  }
  return arr;
};

~

方法2

Array.prototype.map = function (callback, thisArg) {
  if (this == undefined) {
    throw new TypeError('this is null or not defined');
  }

  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  const res = [];
  const O = Object(this);
  const len = O.length >>> 0;

  for (let i = 0; i < len; i++) {
    if (i in O) {
      // 调用回调函数并传入新数组
      res[i] = callback.call(thisArg, O[i], i, this);
    }
  }
  return res;
}


9、用ES5实现数组的filter方法

  • 特点:
  1. 该方法返回一个由通过测试的元素组成的新数组,如果没有通过测试的元素,则返回一个空数组
  2. 回调函数一共接收3个参数,同 map 方法一样。分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」
  • 用法:
let array = [1, 2, 3].filter((item) => {
  return item > 2;
});

console.log(array); // [3]


  • 实现:
Array.prototype.filter = function(fn) {
  let arr = [];
  for(let i = 0; i < this.length; i++) {
    // 执行fn函数,传递this[i],如果fn(this[i])的返回结果是true,就添加到arr
    fn(this[i]) && arr.push(this[i]);
  }
  return arr;
};


10、用ES5实现数组的some方法

  • 特点:
  1. 在数组中查找元素,如果找到一个符合条件的元素就返回true,如果所有元素都不符合条件就返回 false;
  2. 回调函数一共接收3个参数,同 map 方法一样。分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」。
  • 用法:
let flag = [1, 2, 3].some((item) => {
  return item > 1;
});

console.log(flag); // true


  • 实现:
Array.prototype.some = function(fn) {
  for(let i = 0; i < this.length; i++) {
    // 执行fn函数,传递this[i],如果fn(this[i])的返回结果是true,就返回true
    if (fn(this[i])) {
      return true;
    }
  }
  // for 循环结束了,都没有找到就返回false
  return false;
};


11、用ES5实现数组的every方法

  • 特点:
  1. 检测一个数组中的元素是否都能符合条件,都符合条件返回true,有一个不符合则返回 false
  2. 如果收到一个空数组,此方法在任何情况下都会返回 true
  3. 回调函数一共接收3个参数,同 map 方法一样。分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」
  • 用法:
let flag = [1, 2, 3].every((item) => {
  return item > 1;
});

console.log(flag); // false


  • 实现:
Array.prototype.every = function(fn) {
  for(let i = 0; i < this.length; i++) {
    // 执行fn函数,传递this[i],如果所有的fn(this[i])的返回结果是false,就返回false
    if(!fn(this[i])) {
      return false
    }
  }
  return true;
};



12、用ES5实现数组的find方法

  • 特点:
  1. 在数组中查找元素,如果找到符合条件的元素就返回这个元素,如果没有符合条件的元素就返回 undefined,且找到后不会继续查找
  2. 回调函数一共接收3个参数,同 map 方法一样。分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」
  • 用法:
let item = [1, 2, 3].find((item) => {
  return item > 1;
});

console.log(item); // 2


  • 实现:
Array.prototype.find = function(fn) {
  for(let i = 0; i < this.length; i++) {
    // 执行fn函数,传递this[i],如果fn(this[i])的返回结果是true,就返回this[i]
    if (fn(this[i])) return this[i];
  }
  // return undefined // 这句代码可加可不加
};


13、用ES5实现数组的forEach方法

  • 特点:
  1. 循环遍历数组,该方法没有返回值
  2. 回调函数一共接收3个参数,同 map 方法一样。分别是:「正在处理的当前元素的值、正在处理的当前元素的索引、正在遍历的集合对象」
  • 用法:
[1, 2, 3].forEach((item, index, array) => {
  // 1 0 [1, 2, 3]
  // 2 1 [1, 2, 3]
  // 3 2 [1, 2, 3]
  console.log(item, index, array)  
});

  • 实现:
Array.prototype.forEach = function(fn) {
  for(let i = 0; i < this.length; i++) {
    fn(this[i], i, this);
  }
};


14、用ES5实现数组的reduce方法

  • 特点:
  1. 初始值不传时的特殊处理:会默认用数组中的第一个元素

  2. 函数的返回结果会作为下一次循环的 prev

  3. 回调函数一共接收4个参数,分别是「上一次调用回调时返回的值、正在处理的元素、正在处理的元素的索引、正在遍历的集合对象」

    arr.reduce(prev, next, currentIndex, array)
    
    - prev:上一次调用回调时返回的值
    - 正在处理的元素
    - 正在处理的元素的索引
    - 正在遍历的集合对象
    
    
  • 用法:
let total = [1, 2, 3].reduce((prev, next, currentIndex, array) => {
  return prev + next;
}, 0);
console.log(total); // 6


  • 实现:
Array.prototype.reduce = function (fn, prev) {
  for (let i = 0; i < this.length; i++) {
    // 初始值不传时的处理
    if (typeof prev === 'undefined') {
      // 明确回调函数的参数都有哪些
      // 【这里的i + 1,不能写成 ++i 和 i++,】
      prev = fn(this[i], this[i + 1], i + 1, this);
      // 经测试,这句代码注释掉,prev有初始值时,结果没有影响;没有初始值,结果会多加一次数组的第二项
      // 第一次是处理第一、二项,第二次是处理第一次的结果 和 第三项,要 ++i
      ++i;
    } else {
      prev = fn(prev, this[i], i, this)
    }
  }

  return prev; // 函数的返回结果会作为下一次循环的 prev
};

let total = [1, 6, 3].reduce((prev, next, currentIndex, array) => {
  return prev + next;
});
console.log(total); // 6


15、柯理化函数

柯理化函数含义:是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。

// add的参数不固定,看有几个数字累计相加
function add(a, b, c, d) {
  return a + b + c + d
}

function currying(fn, ...args) {
  // fn.length 回调函数的参数的总和
  // args.length currying函数 后面的参数总和 
  // 如:add (a,b,c,d)  currying(add,1,2,3,4)
  if (fn.length === args.length) {
    return fn(...args)
  } else {
    // 继续分步传递参数 newArgs 新一次传递的参数
    return function anonymous(...newArgs) {
      // 将先传递的参数和后传递的参数 结合在一起
      let allArgs = [...args, ...newArgs]
      return currying(fn, ...allArgs)
    }
  }
}

let fn1 = currying(add, 1, 2) 
let fn2 = fn1(3)  
let fn3 = fn2(4)  // 10
console.log(fn1, fn2, fn3) // [Function: anonymous] [Function: anonymous] 10

let res = currying(add)(1, 2, 3, 4) 
console.log(res) // 10


16、实现一个反柯理化函数

  • 特点:

使用callapply可以让非数组借用一些其他类型的函数,比如,Array.prototype.push.call, Array.prototype.slice.calluncrrying把这些方法泛化出来,不在只单单的用于数组,更好的语义化。

  • 用法:
// 利用反柯里化创建检测数据类型的函数
// Object.prototype.toString也是Function的实例,所以可以访问Function.prototype上的方法
let checkType = Object.prototype.toString.unCurring()

console.log(checkType(1)); // [object Number]
console.log(checkType("hello")); // [object String]
console.log(checkType(true)); // [object Boolean]

  • 实现:
Function.prototype.unCurring = function () {
  var self = this;
  return function () {
    // 返回Function.prototype.call方法
    return Function.prototype.call.apply(self, arguments);
  }
};


17、组合compose函数

  • 特点:
  • 将需要嵌套执行的函数平铺,嵌套执行就是一个函数的返回值将作为另一个函数的参数。该函数调用的方向是从右至左的(先执行 sum,再执行 toUpper,再执行 add)。
// funcs: 存储的是最后需要按照顺序依次执行的函数集合
function compose(...funcs) {
  // 要返回一个函数,外面才能传参调用
  return function anonymous(...args) {
    // args: 存储的是给第一个函数执行传递的实参集合
    if (funcs.length === 0) return args.length <= 1 ? args[0] : args;
    if (funcs.length === 1) return funcs[0](...args);
    // funcs = [add1, add1, mul3, div2]
    // args = [0]
    
    return funcs.reduce((result, item) => {
      // 通过判断是否是函数类型
      return typeof result === "function" ?
        item(result(...args)) :
        item(result);
    }); 
   
    // 不用判断是否是函数类型
    /* let n = 0;
    return funcs.reduce((result, item) => {
      n++;
      return n === 1 ? item(result(...args)) : item(result);
    }); */
  };
}

const add1 = (x) => x + 1;
const mul3 = (x) => x * 3;
const div2 = (x) => x / 2;

// => 0 如果不指定任何函数, 直接把最后传递的结果返回,传递一个返回一个值,传递多个返回一个数组
let result = compose()(0);
console.log(result);

// => add1(0) 只指定一个函数,就是把最后的结果传递这个函数,执行函数获取其返回值即可
result = compose(add1)(0);
console.log(result);

result = compose(add1, add1, mul3, div2)(0);
console.log(result);


方法2

  • 用法
function sum(a, b) {
  return a + b;
}

function toUpper(str) {
  return str.toUpperCase();
}

function add(str) {
  return '===' + str + '==='
}

// 使用 compose 之前:
console.log(add(toUpper(sum('cherry', '27')))); // ===CHERRY27===
// 使用 compose 之后:
console.log(compose(add, toUpper, sum)('cherry', '27')); // ===CHERRY27===

// 使用 ES6 - reduce 一行代码实现
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));

// 使用 ES5- reduceRight 实现
function compose(...fns) {
  return function (...args) {
    let lastFn = fns.pop();
    return fns.reduceRight((a, b) => {
      return b(a);
    }, lastFn(...args));
  };
}

// 使用 ES6 - reduceRight 实现
const compose = (...fns) => (...args) => {
  let lastFn = fns.pop();
  return fns.reduceRight((a, b) => b(a), lastFn(...args));
};


18、实现一个 Pipe (管道)

  • 特点:

pipe函数跟compose函数的作用是一样的,也是将参数平铺,只不过他的顺序是从左往右。(先执行 splitString,再执行 count)

  • 用法:
function splitString(str) {
  return str.split(' ');
}

function count(array) {
  return array.length;
}

// 使用 pipe 之前:
console.log(count(splitString('hello cherry'))); // 2
// 使用 pipe 之后:
console.log(pipe(splitString, count)('hello cherry')); // 2


  • 实现:
// 使用 ES6 - reduce 一行代码实现:(redux源码)
const pipe = (...fns) => (...args) => fns.reduce((a, b) => b(a), ...args);

const pipe = function () {
  const args = [].slice.apply(arguments);
  return function (x) {
    return args.reduce((res, cb) => cb(res), x);
  }
}

// 使用 ES5- reduceRight 实现
function pipe(...fns) {
  return function (...args) {
    let lastFn = fns.shift();
    return fns.reduceRight((a, b) => {
      return b(a);
    }, lastFn(...args));
  };
}

// 使用 ES6 - reduceRight 实现
const pipe = (...fns) => (...args) => {
  let lastFn = fns.shift();
  return fns.reduceRight((a, b) => b(a), lastFn(...args));
};


19、数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]


方法一:使用flat()

const res1 = arr.flat(Infinity);


方法二:利用正则

const res2 = JSON.stringify(arr).replace(/[|]/g, '').split(',');

但数据类型都会变为字符串


方法三:正则改良版本

const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/[|]/g, '') + ']');


方法四:使用reduce

const flatten = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const res4 = flatten(arr);


方法五:函数递归

const arr = [1, [2, [3, [4, 5]]], 6];
const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);
console.log(res5) //  [1, 2, 3, 4, 5, 6]

// 我写的
const arr = [1, [2, [3, [4, 5]]], 6];
const res5 = [];

const fn = arr => {
  for (let i = 0; i < arr.length; i++) {
    let item = arr[i]
    if (item instanceof Array) {
      fn(item)
    } else {
      res5.push(item)
    }
  }
}

fn(arr)
console.log(res5)


// 辉森面试题:请将该 data 数组铺平
const data = [
  {
    id: 1,
    title: "课程 1",
    children: [
      { id: 4, title: "课程 1-1" },
      {
        id: 5,
        title: "课程 1-2",
        children: [
          { id: 6, title: "课程 1-2-1" },
          { id: 7, title: "课程 1-2-2" },
        ],
      },
    ],
  },
  { id: 2, title: "课程 2" },
  { id: 3, title: "课程 3" },
];

// 输出结果:
const formatData = [
  { id: 1, title: "课程 1" },
  { id: 4, title: "课程 1-1" },
  { id: 5, title: "课程 1-2" },
  { id: 6, title: "课程 1-2-1" },
  { id: 7, title: "课程 1-2-2" },
  { id: 2, title: "课程 2" },
  { id: 3, title: "课程 3" },
];

function getFormData(data) {
  let res = []
  function handleData(arr) {
    arr.forEach(item => {
      res.push({ id: item.id, title: item.title })
      const children = item.children
      if (children && Array.isArray(children)) {
        handleData(children)
      }
    })
  }

  handleData(data)
  return res
}

let r = getFormData(data)
console.log(r)

// 加的
const arr = []
function fn(data) {
  if (!(data instanceof Array)) return

  data.forEach(item => {
    arr.push({ id: item.id, title: item.title })
    item.children instanceof Array && fn(item.children)
  })
  return arr
}

let res = fn(data)
console.log(res)


20、浅克隆/浅拷贝

浅克隆:只拷贝对象或数组的第一层内容

const shallClone = (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;
  }
}


21、深克隆(考虑日期、正则、循环引用等)

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

function deepClone(target, map = new Map()) {
  // 先判断该引用类型是否被 拷贝过
  if (map.get(target)) {
    return target;
  }
  // 获取当前值的构造函数:获取它的类型
  let constructor = target.constructor;

  // 检测当前对象target是否与 正则、日期格式对象匹配
  if (/^(RegExp|Date)$/i.test(constructor.name)) {
    return new constructor(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;
  }
}


22、实现一个可以拖拽的DIV

<!DOCTYPE html>
<html>

<head lang="en">
  <meta charset="UTF-8">
  <title></title>
  <style>
    .login-header {
       100%;
      text-align: center;
      height: 30px;
      font-size: 24px;
      line-height: 30px;
    }

    ul,
    li,
    ol,
    dl,
    dt,
    dd,
    div,
    p,
    span,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6,
    a {
      padding: 0px;
      margin: 0px;
    }

    .login {
      display: none;
       512px;
      height: 280px;
      position: fixed;
      border: #ebebeb solid 1px;
      left: 50%;
      top: 50%;
      background: #ffffff;
      box-shadow: 0px 0px 20px #ddd;
      z-index: 9999;
      transform: translate(-50%, -50%);
    }

    .login-title {
       100%;
      margin: 10px 0px 0px 0px;
      text-align: center;
      line-height: 40px;
      height: 40px;
      font-size: 18px;
      position: relative;
      cursor: move;
    }

    .login-input-content {
      margin-top: 20px;
    }

    .login-button {
       50%;
      margin: 30px auto 0px auto;
      line-height: 40px;
      font-size: 14px;
      border: #ebebeb 1px solid;
      text-align: center;
    }

    .login-bg {
      display: none;
       100%;
      height: 100%;
      position: fixed;
      top: 0px;
      left: 0px;
      background: rgba(0, 0, 0, .1);
    }

    a {
      text-decoration: none;
      color: #000000;
    }

    .login-button a {
      display: block;
    }

    .login-input input.list-input {
      float: left;
      line-height: 35px;
      height: 35px;
       350px;
      border: #ebebeb 1px solid;
      text-indent: 5px;
    }

    .login-input {
      overflow: hidden;
      margin: 0px 0px 20px 0px;
    }

    .login-input label {
      float: left;
       90px;
      padding-right: 10px;
      text-align: right;
      line-height: 35px;
      height: 35px;
      font-size: 14px;
    }

    .login-title span {
      position: absolute;
      font-size: 12px;
      right: -20px;
      top: -30px;
      background: #ffffff;
      border: #ebebeb solid 1px;
       40px;
      height: 40px;
      border-radius: 20px;
    }
  </style>
</head>

<body>
  <div class="login-header">
    <a id="link" href="javascript:;">点击,弹出登录框</a>
  </div>

  <div id="login" class="login">
    <div id="title" class="login-title">
      登录会员
      <span>
        <a id="closeBtn" href="javascript:void(0);" class="close-login">关闭</a>
      </span>
    </div>

    <div class="login-input-content">
      <div class="login-input">
        <label>用户名:</label>
        <input type="text" placeholder="请输入用户名" name="info[username]" id="username" class="list-input">
      </div>

      <div class="login-input">
        <label>登录密码:</label>
        <input type="password" placeholder="请输入登录密码" name="info[password]" id="password" class="list-input">
      </div>
    </div>

    <div id="loginBtn" class="login-button">
      <a href="javascript:void(0);" id="login-button-submit">登录会员</a>
    </div>
  </div>
  <!-- 遮盖层 -->
  <div id="bg" class="login-bg"></div>

  <script>
    // 1. 获取元素
    var login = document.querySelector('.login');
    var mask = document.querySelector('.login-bg');
    var link = document.querySelector('#link');
    var closeBtn = document.querySelector('#closeBtn');
    var title = document.querySelector('#title');

    // 2. 点击弹出层这个链接 link  让mask 和login 显示出来
    link.addEventListener('click', function () {
      mask.style.display = 'block';
      login.style.display = 'block';
    })

    // 3. 点击 closeBtn 就隐藏 mask 和 login 
    closeBtn.addEventListener('click', function () {
      mask.style.display = 'none';
      login.style.display = 'none';
    })

    // 4. 开始拖拽
    // (1) 当我们鼠标按下, 就获得鼠标在盒子内的坐标
    title.addEventListener('mousedown', function (e) {
      var x = e.pageX - login.offsetLeft;
      var y = e.pageY - login.offsetTop;

      // (2) 鼠标移动的时候,把鼠标在页面中的坐标,减去 鼠标在盒子内的坐标就是模态框的left和top值
      document.addEventListener('mousemove', move)

      function move(e) {
        login.style.left = e.pageX - x + 'px';
        login.style.top = e.pageY - y + 'px';
      }

      // (3) 鼠标弹起,就让鼠标移动事件移除
      document.addEventListener('mouseup', function () {
        document.removeEventListener('mousemove', move);
      })
    })
  </script>
</body>

</html>


23、实现数组的取交集,并集,差集

1. 取交集

Array.prototype.includes

let a = [1, 2, 3];
let b = [2, 4, 5];
let intersection = a.filter(v => b.includes(v));
console.log(intersection); // [ 2 ]


Array.from

let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let intersection = Array.from(new Set(a.filter(v => bSet.has(v))));

console.log(intersection); // [ 2 ]


Array.prototype.indexOf

let a = [1, 2, 3];
let b = [2, 4, 5];
let intersection = a.filter((v) => b.indexOf(v) > -1);

console.log(intersection); // [ 2 ]


2. 取并集

Array.prototype.includes

let a = [1, 2, 3];
let b = [2, 4, 5];

let union = a.concat(b.filter(v => !a.includes(v)));

console.log(union); // [ 1, 2, 3, 4, 5 ]


Array.from

let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let union = Array.from(new Set(a.concat(b)));

console.log(union); // [ 1, 2, 3, 4, 5 ]


Array.prototype.indexOf

let a = [1, 2, 3];
let b = [2, 4, 5];
let union = a.concat(b.filter((v) => a.indexOf(v) === -1));

console.log(union); // [ 1, 2, 3, 4, 5 ]



3. 取差集

Array.prototype.includes

let a = [1, 2, 3];
let b = [2, 4, 5];

let difference = a.concat(b).filter(v => !a.includes(v) || !b.includes(v));

console.log(difference); // [ 1, 3, 4, 5 ]


Array.from

let a = [1, 2, 3];
let b = [2, 4, 5];
let aSet = new Set(a);
let bSet = new Set(b);
let difference = Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v))));

console.log(difference); // [ 1, 3, 4, 5 ]


Array.prototype.indexOf

let a = [1, 2, 3];
let b = [2, 4, 5];
let difference = a.filter((v) => b.indexOf(v) === -1).concat(b.filter((v) => a.indexOf(v) === -1));

console.log(difference); // [ 1, 3, 4, 5 ]


24、实现Object.create方法(经常考)

  • 特点:

创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

  • 用法:
let demo = {
  c : '123'
};
let cc = Object.create(demo);
console.log(cc);


  • 实现:
function create(proto) {
  // 排除传入的对象是 null 和 非object的情况
  if (proto === null || typeof proto !== 'object') {
    throw new TypeError(`Object prototype may only be an Object: ${proto}`);
  }
  
  function Fn() { };
  // 将Fn的原型指向传入的 proto
  Fn.prototype = proto;
  Fn.prototype.constructor = Fn;
  return new Fn();
};

方法2

function create(prototype) {
  // 排除传入的对象是 null 和 非object的情况
  if (prototype === null || typeof prototype !== 'object') {
    throw new TypeError(`Object prototype may only be an Object: ${prototype}`);
  }
  // 让空对象的 __proto__指向 传进来的 对象(prototype)
  // 目标 {}.__proto__ = prototype
  function Temp() { };
  Temp.prototype = prototype;
  return new Temp;
}


25、寄生式组合继承

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <script>
    // 借用父构造函数继承属性
    // 1. 父构造函数
    function Father(uname, age) {
      // this 指向父构造函数的对象实例
      this.uname = uname;
      this.age = age;
    }

    Father.prototype.money = function () {
      console.log(100000);
    };

    // 2.子构造函数 
    function Son(uname, age, score) {
      // (1)this 指向子构造函数的对象实例;(2)不能写成new Father.call(),Father.call is not a constructor,Father.call不是一个构造函数
      Father.call(this, uname, age);
      this.score = score;
    }

    // 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
    // Son.prototype = Father.prototype;

    // Son.prototype = new Father();
    Son.prototype = Object.create(Father.prototype);
    // 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
    Son.prototype.constructor = Son;

    // 这个是子构造函数专门的方法 【子构造函数专门的方法 要写在上面两行代码的后面。】
    Son.prototype.exam = function () {
      console.log('孩子要考试');
    }

    var son = new Son('刘德华', 18, 100);
    console.log(son); // (1)有money、exam方法; (2)Son {uname: "刘德华", age: 18, score: 100}
    console.log(Father.prototype); // (1)有money方法,没有exam方法; (2){money: ƒ, constructor: ƒ}
    console.log(Son.prototype.constructor); //
  </script>
</body>

</html>


26、实现一个 sleep 函数

思路:比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现。

const sleep = (time) => {
  return new Promise(resolve => setTimeout(resolve, time))
}

sleep(1000).then(() => {
    // 这里写你的骚操作
})

// --------------------

const sleep = (time) => {
  return new Promise(resolve => setTimeout(resolve, time))
}

async function sleepAsync() {
  console.log('fuck the code')
  await sleep(1000)
  console.log('fuck the code again')
}

sleepAsync()

// --------------------

// 我的改版
// 如果定时器的回调函数是简写,那么resolve函数不能传参,否则不能实现定时器效果
const fn2 = (t) => new Promise(resolve => setTimeout(resolve(666), t))

// 定时器的回调函数不简写,resolve函数传参,才能实现定时器效果
const fn2 = (t) => {
  return new Promise(resolve => setTimeout(function () {
    resolve(666)
  }, t))
}

async function fn3(t) {
  let res = await fn2(t)
  console.log(res) // 666
  console.log(555) // 555
}

fn3(1000)

~

// Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).then(() => {
  console.log(1)
})

// -----------------------------

// Generator
function* sleepGenerator(time) {
  yield new Promise(function (resolve, reject) {
    setTimeout(resolve, time);
  })
}
sleepGenerator(1000).next().value.then(() => { console.log(1) })

// -----------------------------

// async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

// ES5
function sleep(callback, time) {
  if (typeof callback === 'function')
    setTimeout(callback, time)
}

function output() {
  console.log(1);
}
sleep(output, 1000);


27、类数组转化为数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。


方法一:Array.from

Array.from(document.querySelectorAll('div'))


方法二:扩展运算符

[...document.querySelectorAll('div')]


方法三:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))


方法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));


29、Object.is

Object.is解决的主要是这两个问题:

+0 === -0  // true
NaN === NaN // false
console.log(Object.is(+0, -0)) // false
console.log(Object.is(NaN, NaN)) // true

const is= (x, y) => {
  if (x === y) {
    // +0 和 -0应该不相等
    return x !== 0 || y !== 0 || 1/x === 1/y;
  } else {
    // 取反操作
    return x !== x && y !== y;
  }
}


29、Object.assign

Object.defineProperty(Object, 'assign', {
  value: function (target, ...args) {
    if (target == null) {
      return new TypeError('Cannot convert undefined or null to object');
    }

    // 目标对象需要统一是引用数据类型,若不是会自动转换
    const to = Object(target);
	    
    for (let i = 0; i < args.length; i++) {
      // 每一个源对象
      const nextSource = args[i];
      // args的每一项都得是对象,不能是null
      if (nextSource !== null) {
        // 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的)
        for (const nextKey in nextSource) {
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)){
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  },
  // 不可枚举
  enumerable: false,
  writable: true,
  configurable: true,
})


30、jsonp

script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好,但仅限于GET请求

const jsonp = ({ url, params, callbackName }) => {
  const generateUrl = () => {
    // 判断是否含有参数,用queryString代替下面的dataSrc
    // let queryString = url.indexOf("?") === "-1" ? "?" : "&";

    let dataSrc = '';

    for (let key in params) {
      if (Object.prototype.hasOwnProperty.call(params, key)) {
        dataSrc += `${key}=${params[key]}&`;
      }
    }

    dataSrc += `callback=${callbackName}`;
    return `${url}?${dataSrc}`;
  }

  return new Promise((resolve, reject) => {
    const scriptEle = document.createElement('script');
    scriptEle.src = generateUrl();
    document.body.appendChild(scriptEle);

    window[callbackName] = data => {
      resolve(data);
      document.removeChild(scriptEle);
    }
  })
}

function jsonp(url, params, callback) {
  // 判断是否含有参数
  let queryString = url.indexOf("?") === "-1" ? "?" : "&";

  // 添加参数 【要去掉最后一个&,substr()】
  for (var k in params) {
    if (params.hasOwnProperty(k)) {
      queryString += k + "=" + params[k] + "&";
    }
  }

  // 处理回调函数名 【时间戳更好】
  let random = Math.random().toString().replace(".", "");
  let callbackName = "myJsonp" + random;

  // 添加回调函数
  queryString += "callback=" + callbackName;

  // 构建请求
  let scriptNode = document.createElement("script");
  scriptNode.src = url + queryString;

  window[callbackName] = function () {
    // 调用回调函数
    callback(...arguments);
    // 删除这个引入的脚本
    document.getElementsByTagName("head")[0].removeChild(scriptNode);
  };

  // 发起请求 【应该写到上面】
  document.getElementsByTagName("head")[0].appendChild(scriptNode);
}

import originJSONP from 'jsonp'

export default function jsonp(url, data, option) {
  // 判断url后面是否有问号
  url += (url.indexOf('?') < 0 ? '?' : '&') + param(data)

  // 【这里需要向后套发送请求,是异步的,所以用Promise】
  return new Promise((resolve, reject) => {
    originJSONP(url, option, (err, data) => {
      if (!err) {
        resolve(data) // 成功
      } else {
        reject(err) // 失败
      }
    })
  })
}

// 拼接data
function param(data) {
  let url = ''
  for (var k in data) {
    let value = data[k] !== undefined ? data[k] : ''
    url += `&${k}=${encodeURIComponent(value)}`
  }
  // 删除第一个&,这里的url开始是空字符串
  return url ? url.substring(1) : ''
}


31、ajax

const getJSON = function (url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}


32、图片懒加载

可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;

  for (let i = 0; i < len; i++) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

// 补充
获得页面滚动过的高度:
document.body.scrollTop || document.documentElement.scrollTop
【document.documentElement.scrollTop,这个貌似比document.body.scrollTop管用。】


offsetWidth:  width + padding + border (披着羊皮的狼) 
clientWidth: width  +  padding, 不包含border。      
scrollWidth:  如果内容没有超出盒子,就是width + padding。如果超过盒子宽度,就是内容的宽度。
高度同理。


offset 概述
offset 翻译过来就是偏移量, 我们使用 offset系列相关属性可以动态的得到该元素的位置(偏移)、大小等。
1. 获得元素距离带有定位父元素的位置
2. 获得元素自身的大小(宽度高度)
3. 注意:返回的数值都不带单位
4. offsetLeft:它以带有定位的父亲为准,如果没有父亲或者父亲没有定位,则以 body 为准

img


33.滚动加载

原理就是监听页面滚动事件,分析clientHeightscrollTopscrollHeight三者的属性关系。

window.addEventListener('scroll', function() {
  const clientHeight = document.documentElement.clientHeight;
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  if (clientHeight + scrollTop >= scrollHeight) {
    // 检测到滚动至页面底部,进行后续操作
    // ...
  }
}, false);

一个Demo:页面滚动加载的Demo


34.渲染几万条数据不卡住页面

渲染大数据时,合理使用createDocumentFragmentrequestAnimationFrame,将操作切分为一小段一小段执行。

setTimeout(() => {
  const total = 100000; // 插入十万条数据
  const once = 20; // 一次插入的数据
  const loopCount = Math.ceil(total / once); // 插入数据需要的次数
  let countOfRender = 0;
  let index = 0; // 我加的
  const ul = document.querySelector('ul');

  // 添加数据的方法
  function add() {
    const fragment = document.createDocumentFragment();

    for (let i = 0; i < once; i++) {
      const li = document.createElement('li');
      // li.innerText = Math.floor(Math.random() * total);
      li.innerText = ++index; // 我改的,用递增的数字更直观
      fragment.appendChild(li);
    }

    ul.appendChild(fragment);
    countOfRender += 1;
    loop();
  }

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


35.打印出当前网页使用了多少种HTML元素

一行代码可以解决:

const fn = () => {
  return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;
}

值得注意的是:DOM操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。


36、Object.freeze

Object.freeze:冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象。

function myFreeze(obj) {
  // 判断参数是否为Object类型,如果是就封闭对象,循环遍历对象。去掉原型属性,将其writable特性设置为false
  if (obj instanceof Object) {
    // Object.seal():封闭一个对象,阻止添加新属性,并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。
    Object.seal(obj);
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        Object.defineProperty(obj, key, {
          writable: false   // 设置只读
        })
        // 如果属性值依然为对象,要通过递归来进行进一步的冻结
        myFreeze(obj[key]);
      }
    }
  }
}


37、JS实现String.trim()方法

  • trim本质就是在字符串的前后去掉空格
String.prototype.emuTrim = function() {
  return this.replace(/(^s*)|(s*$)/g, '')
}

原文地址:https://www.cnblogs.com/jianjie/p/13902421.html