前言
这篇文章主要是 总结 + 实践 一些比较常见且重要的JS手写题,方便自己以及大家学习参考。
1. 数据类型判断
核心思想:typeof
可以判断 Undefined、String、Number、Boolean、Symbol、Function
类型的数据,但对其他的都会认为是Object
,比如Null、Array
等。所以通过typeof
来判断数据类型会不准确。
解决方法:可以通过Object.prototype.toString
解决。
实现:
function mytypeof(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
复制代码
- 使用
call
是为了绑定this
到obj
上 - 使用
slice
是因为这前面返回的结果是类似[Object xxx]
这样的,xxx
是根据obj
的类型变化的 - 使用
toLowerCase
是因为原生typeof
的返回结果的第一个字母是小写字母。
2. 继承
2.1 原型继承
核心思想:子类的原型成为父类的实例
实现:
function SuperType() {
this.colors = ['red', 'green'];
}
function SubType() {}
// 原型继承关键: 子类的原型成为父类的实例
SubType.prototype = new SuperType();
// 测试
let instance1 = new SubType();
instance1.colors.push('blue');
let instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue']
复制代码
原型继承存在的问题:
- 原型中包含的引用类型属性将被所有实例对象共享
- 子类在实例化时不能给父类构造函数传参
2.2 构造函数继承
核心思想:在子类构造函数中调用父类构造函数
实现:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
this.getName = function() {
return this.name;
}
}
function SubType(name) {
// 继承 SuperType 并传参
SuperType.call(this, name);
}
// 测试
let instance1 = new SubType('instance1');
instance1.colors.push('blue');
console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2');
console.log(instance2.colors); // ['red', 'green']
复制代码
构造函数继承的出现是为了解决了原型继承的引用值共享问题。优点是可以在子类构造函数中向父类构造函数传参。它存在的问题是:1)由于方法必须在构造函数中定义,因此方法不能重用。2)子类也不能访问父类原型上定义的方法。
2.3 组合继承
核心思想:综合了原型链和构造函数,即,使用原型链继承原型上的方法,而通过构造函数继承实例属性。
实现:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
// 实例属性
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
// 测试
let instance1 = new SubType('instance1', 1);
instance1.sayName(); // "instance1"
instance1.colors.push("blue");
console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2', 2);
instance2.sayName(); // "instance2"
console.log(instance2.colors); // ['red','green']
复制代码
组合继承存在的问题是:父类构造函数始终会被调用两次:一次是在创建子类原型时new SuperType()
调用,另一次是在子类构造函数中SuperType.call()
调用。
2.4 寄生式组合继承(最佳)
核心思想:通过构造函数继承属性,但使用混合式原型继承方法,即,不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。
实现:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'green'];
}
Super.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = Object.create(SuperType.prototype);
// 重写原型导致默认 constructor 丢失,手动将 constructor 指回 SubType
SubType.prototype.constructor = SubType;
复制代码
2.5 class 实现继承(ES6)
核心思想:通过 extends
来实现类的继承(相当于 ES5
的原型继承)。通过 super
调用父类的构造方法 (相当于 ES5
的构造函数继承)。
实现:
class SuperType {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class SubType extends SuperType {
constructor(name, age) {
super(name); // 继承属性
this.age = age;
}
}
// 测试
let instance = new SubType('instance', 0);
instance.sayName(); // "instance"
复制代码
虽然类继承使用的是新语法,但背后依旧使用的是原型链。
3. 数组去重
3.1 使用 indexOf/includes 实现
function unique(arr) {
var res = [];
for(var i = 0; i < arr.length; i++) {
if(res.indexOf(arr[i]) === -1) res.push(arr[i]);
// if(!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
复制代码
3.2 使用 filter(forEach) + indexOf/includes 实现
// filter
function unique(arr) {
var res = arr.filter((value, index) => {
// 只存第一个出现的元素
return arr.indexOf(value) === index;
});
return res;
}
// forEach
function unique(arr) {
var res = [];
arr.forEach((value) => {
if(!res.includes(value)) res.push(value);
});
return res;
}
复制代码
3.3 非 API 版本(原生)实现
function unique(arr) {
var res = [];
for(var i = 0; i < arr.length; i++) {
var flag = false;
for(var j = 0; j < res.length; j++) {
if(arr[i] === res[j]) {
flag = true;
break;
}
}
if(flag === false) res.push(arr[i]);
}
return res;
}
复制代码
3.4 ES6 使用 Set + 扩展运算符(...)/Array.from() 实现
function unique(arr) {
// return [...new Set(arr)];
return Array.from(new Set(arr));
}
复制代码
4. 数组扁平化
4.1 ES5 递归写法 —— isArray()、concat()
function flat11(arr) {
var res = [];
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flat11(arr[i]));
} else {
res.push(arr[i]);
}
}
return res;
}
复制代码
如果想实现第二个参数(指定“拉平”的层数),可以这样实现,后面的几种可以自己类似实现:
function flat(arr, level = 1) {
var res = [];
for(var i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i]) || level >= 1) {
res = res.concat(flat(arr[i]), level - 1);
}
else {
res.push(arr[i]);
}
}
return res;
}
复制代码
4.2 ES6 递归写法 — reduce()、concat()、isArray()
function flat(arr) {
return arr.reduce(
(pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur) : cur), []
);
}
复制代码
4.3 ES6 迭代写法 — 扩展运算符(...)、some()、concat()、isArray()
ES6 的扩展运算符(...) 只能扁平化一层
function flat(arr) {
return [].concat(...arr);
}
复制代码
全部扁平化:遍历原数组,若arr
中含有数组则使用一次扩展运算符,直至没有为止。
function flat(arr) {
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
复制代码
4.4 toString/join & split
调用数组的 toString()/join()
方法(它会自动扁平化处理),将数组变为字符串然后再用 split
分割还原为数组。由于 split
分割后形成的数组的每一项值为字符串,所以需要用一个map
方法遍历数组将其每一项转换为数值型。
function flat(arr){
return arr.toString().split(',').map(item => Number(item));
// return arr.join().split(',').map(item => Number(item));
}
复制代码
4.5 使用正则
JSON.stringify(arr).replace(/[|]/g, '')
会先将数组arr
序列化为字符串,然后使用 replace()
方法将字符串中所有的[
或 ]
替换成空字符,从而达到扁平化处理,此时的结果为 arr
不包含 []
的字符串。最后通过JSON.parse()
解析字符串。
function flat(arr) {
return JSON.parse("[" + JSON.stringify(arr).replace(/\[|\]/g,'') + "]");
}
复制代码
5. 类数组转化为数组
类数组是具有 length
属性,但不具有数组原型上的方法。常见的类数组有 arguments
、DOM 操作方法返回的结果(如document.querySelectorAll('div')
)等。
5.1 扩展运算符(...)
注意:扩展运算符只能作用于 iterable
对象,即拥有 Symbol(Symbol.iterator)
属性值。
let arr = [...arrayLike]
复制代码
5.2 Array.from()
let arr = Array.from(arrayLike);
复制代码
5.3 Array.prototype.slice.call()
let arr = Array.prototype.slice.call(arrayLike);
复制代码
5.4 Array.apply()
let arr = Array.apply(null, arrayLike);
复制代码
5.5 concat + apply
let arr = Array.prototype.concat.apply([], arrayLike);
复制代码
6. 实现数组原型方法
6.1 forEach
语法:
arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
参数:
callback
:为数组中每个元素执行的函数,该函数接受1-3个参数
currentValue
: 数组中正在处理的当前元素
index
(可选): 数组中正在处理的当前元素的索引
array
(可选):forEach()
方法正在操作的数组
thisArg
(可选): 当执行回调函数callback
时,用作this
的值。返回值:
undefined
Array.prototype.forEach1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
// 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。
const O = Object(this);
// O.length >>> 0 无符号右移 0 位
// 意义:为了保证转换后的值为正整数。
// 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型
const len = O.length >>> 0;
let k = 0;
while(k < len) {
if(k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
复制代码
6.2 map
语法:
arr.map(callback(currentValue [, index [, array]])[, thisArg])
参数:与
forEach()
方法一样返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
Array.prototype.map1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let newArr = []; // 返回的新数组
let k = 0;
while(k < len) {
if(k in O) {
newArr[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
return newArr;
}
复制代码
6.3 filter
语法:
arr.filter(callback(element [, index [, array]])[, thisArg])
参数:
callback
: 用来测试数组的每个元素的函数。返回true
表示该元素通过测试,保留该元素,false
则不保留。它接受以下三个参数:element、index、array
,参数的意义与forEach
一样。
thisArg
(可选): 执行callback
时,用于this
的值。返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。
Array.prototype.filter1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let newArr = []; // 返回的新数组
let k = 0;
while(k < len) {
if(k in O) {
if(callback.call(thisArg, O[k], k, O)) {
newArr.push(O[k]);
}
}
k++;
}
return newArr;
}
复制代码
6.4 some
语法:
arr.some(callback(element [, index [, array]])[, thisArg])
参数:
callback
: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。
thisArg
(可选): 执行callback
时,用于this
的值。返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false。
Array.prototype.some1 = function(callback, thisArg) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
while(k < len) {
if(k in O) {
if(callback.call(thisArg, O[k], k, O)) {
return true
}
}
k++;
}
return false;
}
复制代码
6.5 reduce
语法:
arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])
参数:
callback
: 一个 “reducer” 函数,包含四个参数:
preVal
:上一次调用callback
时的返回值。在第一次调用时,若指定了初始值initialValue
,其值则为initialValue
,否则为数组索引为 0 的元素array[0]
。
curVal
:数组中正在处理的元素。在第一次调用时,若指定了初始值initialValue
,其值则为数组索引为 0 的元素array[0]
,否则为array[1]
。
curIndex
(可选):数组中正在处理的元素的索引。若指定了初始值initialValue
,则起始索引号为 0,否则从索引 1 起始。
array
(可选):用于遍历的数组。initialValue(可选): 作为第一次调用
callback
函数时参数preVal
的值。若指定了初始值initialValue
,则curVal
则将使用数组第一个元素;否则preVal
将使用数组第一个元素,而curVal
将使用数组第二个元素。返回值:使用 “reducer” 回调函数遍历整个数组后的结果。
Array.prototype.reduce1 = function(callback, initialValue) {
if(this == null) {
throw new TypeError('this is null or not defined');
}
if(typeof callback !== "function") {
throw new TypeError(callback + 'is not a function');
}
const O = Object(this);
const len = O.length >>> 0;
let k = 0;
let accumulator = initialValue;
// 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值
if(accumulator === undefined) {
while(k < len && !(k in O)) {
k++;
}
// 如果超出数组界限还没有找到累加器的初始值,则TypeError
if(k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = O[k++];
}
while(k < len) {
if(k in O) {
accumulator = callback(null, accmulator, O[k], k, O);
}
k++;
}
return accumulator;
}
复制代码
7. call/apply/bind 的实现
7.1 call
描述:使用 一个指定的 this
值(默认为 window
) 和 一个或多个参数 来调用一个函数。
语法:function.call(thisArg, arg1, arg2, ...)
核心思想:
- 调用call 的可能不是函数
- this 可能传入 null
- 传入不固定个数的参数
- 给对象绑定函数并调用
- 删除绑定的函数
- 函数可能有返回值
实现:
Function.prototype.call1 = function(context, ...args) {
if(typeof this !== "function") {
throw new TypeError("this is not a function");
}
context = context || window; // 如果传入的是null, 则指向window
let fn = Symbol('fn'); // 创造唯一的key值,作为构造的context内部方法名
context[fn] = this; // 为 context 绑定原函数(this)
let res = context[fn](...args); // 调用原函数并传参, 保存返回值用于call返回
delete context[fn]; // 删除对象中的函数, 不能修改对象
return res;
}
复制代码
7.2 apply
描述:与 call
类似,唯一的区别就是 call
是传入不固定个数的参数,而 apply
是传入一个参数数组或类数组。
实现:
Function.prototype.apply1 = function(context, arr) {
if(typeof this !== "function") {
throw new TypeError("this is not a function");
}
context = context || window; // 如果传入的是null, 则指向window
let fn = Symbol('fn'); // 创造唯一的key值,作为构造的context内部方法名
context[fn] = this; // 为 context 绑定原函数(this)
let res;
// 判断是否传入的数组是否为空
if(!arr) {
res = context[fn]();
}
else {
res = context[fn](...arr); // 调用原函数并传参, 保存返回值用于call返回
}
delete context[fn]; // 删除对象中的函数, 不能修改对象
return res;
}
复制代码
7.3 bind
描述:bind
方法会创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
核心思想:
- 调用bind的可能不是函数
- bind() 除了 this 外,还可传入多个参数
- bind 创建的新函数可能传入多个参数
- 新函数可能被当做构造函数调用
- 函数可能有返回值
实现:
Function.prototype.bind1 = function(context, ...args) {
if (typeof that !== "function") {
throw new TypeError("this is not function");
}
let that = this; // 保存原函数(this)
return function F(...innerArgs) {
// 判断是否是 new 构造函数
// 由于这里是调用的 call 方法,因此不需要判断 context 是否为空
return that.call(this instanceof F ? this : context, ...args, ...innerArgs);
}
}
复制代码
8. new 实现
描述:new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
核心思想:
- new 会产生一个新对象
- 新对象需要能够访问到构造函数的属性,所以需要重新指定它的原型
- 构造函数可能会显示返回对象与基本类型的情况(以及null)
步骤:使用new
命令时,它后面的函数依次执行下面的步骤:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的隐式原型(
__proto__
),指向构造函数的prototype
属性。 - 让函数内部的
this
关键字指向这个对象。开始执行构造函数内部的代码(为这个新对象添加属性)。 - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
实现:
// 写法一:
function myNew() {
// 将 arguments 对象转为数组
let args = [].slice.call(arguments);
// 取出构造函数
let constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
let obj = {};
obj.__proto__ = constructor.prototype;
// 执行构造函数并将 this 绑定到新创建的对象上
let res = constructor.call(obj, ...args);
// let res = constructor.apply(obj, args);
// 判断构造函数执行返回的结果。如果返回结果是引用类型,就直接返回,否则返回 obj 对象
return (typeof res === "object" && res !== null) ? res : obj;
}
// 写法二:constructor:构造函数, ...args:构造函数参数
function myNew(constructor, ...args) {
// 生成一个空对象,继承构造函数的 prototype 属性
let obj = Object.create(constructor.prototype);
// 执行构造函数并将 this 绑定到新创建的对象上
let res = constructor.call(obj, ...args);
// let res = constructor.apply(obj, args);
// 判断构造函数执行返回的结果。如果返回结果是引用类型,就直接返回,否则返回 obj 对象
return (typeof res === "object" && res !== null) ? res : obj;
}
复制代码
9. 函数柯里化
柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。
对于已经柯里化后的函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数; 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。
9.1 原函数形参定长(此时 fn.length
是个不变的常数)
// 写法1-不保存参数,递归局部函数
function curry(fn) {
let judge = (...args) => {
// 递归结束条件
if(args.length === fn.length) return fn(...args);
return (...arg) => judge(...args, ...arg);
}
return judge;
}
// 写法2-保存参数,递归整体函数
function curry(fn) {
// 保存参数,除去第一个函数参数
let presentArgs = [].slice.call(arguments, 1);
// 返回一个新函数
return function(){
// 新函数调用时会继续传参
let allArgs = [...presentArgs, ...arguments];
// 递归结束条件
if(allArgs.length === fn.length) {
// 如果参数够了,就执行原函数
return fn(,,,allArgs);
}
// 否则继续柯里化
else return curry(fn, ...allArgs);
}
}
// 测试
function add(a, b, c, d) {
return a + b + c + d;
}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
// 以下结果都返回 10
console.log(addCurry(1)(2)(3)(4));
console.log(addCurry(1)(2, 3, 4));
console.log(addCurry(1, 2)(3)(4));
console.log(addCurry(1, 2)(3, 4));
console.log(addCurry(1, 2, 3)(4));
console.log(addCurry(1, 2, 3, 4));
复制代码
9.2 原函数形参不定长(此时 fn.length
为0)
function curry(fn) {
// 保存参数,除去第一个函数参数
const args = [].slice.call(arguments, 1);
// 返回一个新函数
let curried = function () {
// 新函数调用时会继续传参
let allArgs = [...presetArgs, ...arguments];
return curry(fn, ...allArgs);
};
// 利用toString隐式转换的特性,当最后执行函数时,会隐式转换
curried.toString = function () {
return fn(...args);
};
return curried;
}
// 测试
function add(...args) {
return args.reduce((pre, cur) => pre + cur, 0);
}
console.log(add(1, 2, 3, 4));
let addCurry = curry(add);
console.log(addCurry(1)(2)(3) == 6); // true
console.log(addCurry(1, 2, 3)(4) == 10); // true
console.log(addCurry(2, 6)(1).toString()); // 9
console.log(addCurry(2, 6)(1, 8)); // 打印 curried 函数
复制代码
10. 防抖和节流
10.1 防抖
防抖(debounce
):触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。类似王者荣耀的回城功能,你反复触发回城功能,那么只认最后一次,从最后一次触发开始计时。
核心思想:每次事件触发就清除原来的定时器,建立新的定时器。使用apply或call调用传入的函数。函数内部支持使用 this 和 event 对象;
应用:防抖常应用于用户进行搜索输入节约请求资源,window
触发resize
事件时进行防抖只触发一次。
实现:
function debounce(fn, delay) {
// 利用闭包的原理
let timer = null;
return function(...args){
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
// 改变 this 指向为调用 debounce 所指的对象
fn.call(this, ...args);
// fn.apply(this, args);
}, delay);
}
}
复制代码
10.2 节流
节流(throttle
):触发高频事件,且 N 秒内只执行一次。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!类似qq飞车的复位按钮。
核心思想:使用时间戳或标志来实现,立即执行一次,然后每 N 秒执行一次。如果N秒内触发则直接返回。
应用:节流常应用于鼠标不断点击触发、监听滚动事件。
实现:
// 版本一:标志实现
function throttle(fn, wait){
let flag = true; // 设置一个标志
return function(...args){
if(!flag) return;
flag = false;
setTimeout(() => {
fn.call(this, ...args);
flag = true;
}, wait);
}
}
// 版本二:时间戳实现
function throttle(fn, wait) {
let pre = 0;
return function(...args) {
let now = new Date();
if(now - pre < wait) return;
pre = now;
fn.call(this, ...args);
}
}
复制代码
11. instanceof
作用:判断对象的具体类型。可以区别 array 和 object, null 和 object 等。
语法:A instanceof B
如何判断的?: 如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false。
注意:如果检测原始值,则始终返回 false。
实现:
function myinstanceof(left, right) {
// 基本数据类型都返回 false,注意 typeof 函数 返回"function"
if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
let leftPro = left.__proto__; // 取左边的(隐式)原型 __proto__
// left.__proto__ 等价于 Object.getPrototypeOf(left)
while(true) {
// 判断是否到原型链顶端
if(leftPro === null) return false;
// 判断右边的显式原型 prototype 对象是否在左边的原型链上
if(leftPro === right.prototype) return true;
// 原型链查找
leftPro = leftPro.__proto__;
}
}
复制代码
12. 深/浅拷贝
首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。
function isObject(obj) {
return typeof obj === "object" && obj !== null;
}
复制代码
这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。
12.1 浅拷贝(只考虑对象类型)
function shallowClone(obj) {
if(!isObject(obj)) return obj;
let newObj = Array.isArray(obj) ? [] : {};
// for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
for(let key in obj) {
// obj.hasOwnProperty() 方法只考虑对象自身的属性
if(obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
复制代码
12.2 深拷贝
实现一:不考虑 Symbol
function deepClone(obj) {
if(!isObject(obj)) return obj;
let newObj = Array.isArray(obj) ? [] : {};
// for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
for(let key in obj) {
// obj.hasOwnProperty() 方法只考虑对象自身的属性
if(obj.hasOwnProperty(key)) {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
复制代码
实现二:考虑 Symbol
// hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈
function deepClone(obj, hash = new WeakMap()) {
if(!isObject(obj)) return obj;
// 检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象
if(hash.has(obj)) return hash.get(obj);
let newObj = Array.isArray(obj) ? [] : {};
// 备份存在hash中,newObj目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈
hash.set(obj, newObj);
// Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
Reflect.ownKeys(obj).forEach(key => {
// 属性值如果是对象,则进行递归深拷贝,否则直接拷贝
newObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];
});
return newObj;
}
复制代码
13. Object.assign()
描述:Object.assign()
方法用于将所有可枚举(Object.propertyIsEnumerable()
返回 true
)和自有(Object.hasOwnProperty()
返回 true
)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。
实现:
Object,assign = function(target, ...source) {
if(target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
let res = Object(target);
source.forEach(function(obj) {
if(obj != null) {
// for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
// hasOwnProperty 方法只考虑对象自身的属性
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
res[key] = obj[key];
}
}
}
});
return res;
}
复制代码
14. Object.is()
描述:Object.is
不会转换被比较的两个值的类型,这点和===
更为相似,他们之间也存在一些区别。
NaN
在===
中是不相等的,而在Object.is
中是相等的+0
和-0
在===
中是相等的,而在Object.is
中是不相等的
实现:利用 ===
Object.is = function(x, y) {
if(x === y) {
// 当前情况下,只有一种情况是特殊的,即 +0 -0
// 如果 x !== 0,则返回true
// 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
return x !== 0 || 1 / x === 1 / y;
}
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
// x和y同时为NaN时,返回true
return x !== x && y !== y;
}
复制代码
15. 发布订阅模式(事件总线)
描述:实现一个发布订阅模式,拥有 on, emit, once, off
方法
class EventEmitter {
constructor() {
// 包含所有监听器函数的容器对象
// 内部结构: {msg1: [listener1, listener2], msg2: [listener3]}
this.cache = {};
}
// 实现订阅
on(name, callback) {
if(this.cache[name]) {
this.cache[name].push(callback);
}
else {
this.cache[name] = [callback];
}
}
// 删除订阅
off(name, callback) {
if(this.cache[name]) {
this.cache[name] = this.cache[name].filter(item => item !== callback);
}
if(this.cache[name].length === 0) delete this.cache[name];
}
// 只执行一次订阅事件
once(name, callback) {
callback();
this.off(name, callback);
}
// 触发事件
emit(name, ...data) {
if(this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice();
for(let fn of tasks) {
fn(...data);
}
}
}
}
复制代码
16. 实现 JSONP 跨域
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;
实现:
const jsonp = (url, params, callbackName) => {
const generateUrl = () => {
let dataSrc = "";
for(let key in params) {
if(params.hasOwnProperty(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);
}
});
}
复制代码
17. AJAX
实现:利用 XMLHttpRequest
// get
const getJSON = (url) => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
// open 方法用于指定 HTTP 请求的参数: method, url, async(是否异步,默认true)
xhr.open("GET", url, false);
xhr.setRequestHeader('Content-Type', 'application/json');
// onreadystatechange 属性指向一个监听函数。
// readystatechange 事件发生时(实例的readyState属性变化),就会执行这个属性。
xhr.onreadystatechange = function(){
// 4 表示服务器返回的数据已经完全接收,或者本次接收已经失败
if(xhr.readyState !== 4) return;
// 请求成功,基本上只有2xx和304的状态码,表示服务器返回是正常状态
if(xhr.status === 200 || xhr.status === 304) {
// responseText 属性返回从服务器接收到的字符串
resolve(xhr.responseText);
}
// 请求失败
else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
});
}
// post
const postJSON = (url, data) => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
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(data);
});
}
复制代码
18. Promise
Promise的实现参考如下链接
18.1 Promise.resolve
Promise.resolve = function(value) {
// 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象
if(value instanceof Promise) return value;
// 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法
if(typeof value === "object" && 'then' in value) {
return new Promise((resolve, reject) => {
value.then(resolve, reject);
});
}
// 3.否则返回一个新的 Promise 对象,状态为 fulfilled
return new Promise(resolve => resolve(value));
}
复制代码
18.2 Promise.reject
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason));
}
复制代码
18.3 Promise.all
描述:所有 promise
的状态都变成 fulfilled
,就会返回一个状态为 fulfilled
的数组(所有promise
的 value
)。只要有一个失败,就返回第一个状态为 rejected
的 promise
实例的 reason
。
实现:
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
if(Array.isArray(promises)) {
if(promises.length === 0) return resolve(promises);
let result = [];
let count = 0;
promises.forEach((item, index) => {
Promise.resolve(item).then(
value => {
count++;
result[index] = value;
if(count === promises.length) resolve(result);
},
reason => reject(reason)
);
})
}
else return reject(new TypeError("Argument is not iterable"));
});
}
复制代码
18.4 Promise.allSettled
描述:等到所有promise都返回结果,就返回一个promise实例。
实现:
Promise.allSettled = function(promises) {
return new Promise((resolve, reject) => {
if(Array.isArray(promises)) {
if(promises.length === 0) return resolve(promises);
let result = [];
let count = 0;
promises.forEach((item, index) => {
Promise.resolve(item).then(
value => {
count++;
result[index] = {
status: 'fulfilled',
value: value
};
if(count === promises.length) resolve(result);
},
reason => {
count++;
result[index] = {
status: 'rejected'.
reason: reason
};
if(count === promises.length) resolve(result);
}
);
});
}
else return reject(new TypeError("Argument is not iterable"));
});
}
复制代码
18.5 Promise.race
描述:只要promises中有一个率先改变状态,就返回这个率先改变的Promise实例的返回值
实现:
Promise.race = function(promises){
return new Promise((resolve, reject) => {
if(Array.isArray(promises)) {
if(promises.length === 0) return resolve(promises);
promises.forEach((item) => {
Promise.resolve(item).then(
value => resolve(value),
reason => reject(reason)
);
})
}
else return reject(new TypeError("Argument is not iterable"));
});
}
复制代码
18.6 Promise.any
描述:只要promises中有一个fulfilled,就返回第一个fulfilled的Promise实例的返回值。
实现
Promise.any = function(promises) {
return new Promise((resolve, reject) => {
if(Array.isArray(promises)) {
if(promises.length === 0) return reject(new AggregateError("All promises were rejected"));
let count = 0;
promises.forEach((item, index) => {
Promise.resolve(item).then(
value => resolve(value),
reason => {
count++;
if(count === promises.length) {
reject(new AggregateError("All promises were rejected"));
};
}
);
})
}
else return reject(new TypeError("Argument is not iterable"));
});
}
复制代码
19. 异步任务调度器
描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit
个。
实现:
class Scheduler {
queue = []; // 用队列保存正在执行的任务
runCount = 0; // 计数正在执行的任务个数
constructor(limit) {
this.maxCount = limit || 2; // 允许并发的最大个数(默认为2)
}
add(time, data){
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(data);
resolve();
}, time);
});
}
this.queue.push(promiseCreator);
this.request();
}
request() {
if(this.queue.length && this.runCount < this.maxCount) {
this.runCount++;
this.queue.shift()().then(() => {
this.runCount--;
this.request();
});
}
}
}
// 测试
const scheduler = new Scheduler(2);
const addTask = (time, data) => {
scheduler.add(time, data);
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
复制代码
20. setTimeout 模拟 setInterval
描述:使用setTimeout
模拟实现setInterval
的功能。
实现:
const mySetInterval(fn, time) {
let timer = null;
const interval = () => {
timer = setTimeout(() => {
fn(); // time 时间之后会执行真正的函数fn
interval(); // 同时再次调用interval本身
}, time)
}
interval(); // 开始执行
// 返回用于关闭定时器的函数
return () => clearTimeout(timer);
}
// 测试
const cancel = mySetInterval(() => console.log(1), 400);
setTimeout(() => {
cancel();
}, 1000);
// 打印两次1
复制代码
21. setInterval 模拟 setTimeout
描述:使用setInterval
模拟实现setTimeout
的功能。
思路:setTimeout
的特性是在指定的时间内只执行一次,我们只要在setInterval
内部执行 callback
之后,把定时器关掉即可。
实现:
const mySetTimeout = (fn, time) => {
let timer = null;
timer = setInterval(() => {
// 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
clearInterval(timer);
fn();
}, time);
// 返回用于关闭定时器的方法
return () => clearInterval(timer);
}
// 测试
const cancel = mySetTimeout(() => {
console.log(1);
}, 1000);
// 一秒后打印 1
复制代码
22. 图片懒加载
实现:getBoundClientRect
的实现方式,监听 scroll
事件(建议给监听事件添加节流),图片加载完会从 img
标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。
// scr 加载默认图片,data-src 保存实施懒加载后的图片
// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />
let imgs = [...document.querySelectorAll("img")];
const len = imgs.length;
let lazyLoad = function() {
let count = 0;
let deleteImgs = [];
// 获取当前可视区的高度
let viewHeight = document.documentElement.clientHeight;
// 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop)
let scrollTop = window.pageYOffset;
imgs.forEach((img) => {
// 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离
let bound = img.getBoundingClientRect();
// 当前图片距离网页顶部的距离
// let imgOffsetTop = img.offsetTop;
// 判断图片是否在可视区内,如果在就加载(两种判断方式)
// if(imgOffsetTop < scrollTop + viewHeight)
if (bound.top < viewHeight) {
img.src = img.dataset.src; // 替换待加载的图片 src
count++;
deleteImgs.push(img);
// 最后所有的图片加载完毕后需要解绑监听事件
if(count === len) {
document.removeEventListener("scroll", imgThrottle);
}
}
});
// 图片加载完会从 `img` 标签组成的 DOM 列表中删除
imgs = imgs.filter((img) => !deleteImgs.includes(img));
}
window.onload = function () {
lazyLoad();
};
// 使用 防抖/节流 优化一下滚动事件
let imgThrottle = debounce(lazyLoad, 1000);
// 监听 `scroll` 事件
window.addEventListener("scroll", imgThrottle);
复制代码
23. 虚拟DOM转换成真实DOM
描述:将如下 JSON格式的虚拟DOM结构转换成真实DOM结构。
// vnode 结构
{
tag: 'DIV',
attrs: {
id: "app"
},
children: [
{
tag: 'SPAN',
children: [
{
tag: 'A',
children: []
}
]
}
]
}
// 真实DOM 结构
<div id="app">
<span>
<a></a>
</span>
</div>
复制代码
实现:
function _render(vnode) {
// 如果是数字类型转化为字符串;
if(typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if(typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通 DOM
const dom = document.createElement(vnode.tag);
if(vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
dom.setAttribute(key, vnode.attrs[key]);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
复制代码
24. 实现模板字符串解析
描述:实现函数使得将 template
字符串中的{{}}
内的变量替换,如果属性不存在保持原样。
核心:使用字符串替换方法 str.replace(regexp|substr, newSubStr|function)
,使用正则匹配代换字符串。
实现:
function render(template, data) {
// 模板字符串正则 /\{\{\w+\}\}/ ,加 g 为全局匹配模式,每次匹配都会调用后面的函数
let computed = template.replace(/\{\{\w+\}\}/g, function(match, key) {
// match: 匹配的子串; key:括号匹配的字符串
return data[key];
});
return computed;
}
// 测试
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: "张三",
age: 18
}
render(template, data); // 我是张三,年龄18,性别undefined
复制代码
25. 单例模式
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
实现:
var Singleton = (function() {
// 如果在内部声明 SingletonClass 对象,则无法在外部直接调用
var SingletonClass = function() { };
var instance;
return function() {
// 如果已存在,则返回 instance
if(instance) return instance;
// 如果不存在,则new 一个 HeadClass 对象
instance = new SingletonClass();
return instance;
}
})();
// 测试
var a = new Singleton();
var b = new Singleton();
console.log(a === b); // true
复制代码