首页 > js函数手写(一)
头像
Red_Ferrari
编辑于 2021-06-10 23:45
+ 关注

js函数手写(一)

目录

js函数手写(一)

js函数手写(二)

1.手写call

Function.prototype.myCall = function (context = window, ...args) {
    let fn = Symbol();
    context[fn] = this;
    let result = context[fn](...args);
    delete context[fn];
    return result;
}

2.手写apply

Function.prototype.myApply = function (context = window, args) {
    let fn = Symbol();
    context[fn] = this;
    let result = context[fn](...args);
    delete context[fn];
    return result;
}

3.手写bind

Function.prototype.myBind = function (context = window, ...args) {
    let fn = Symbol();
    context[fn] = this;
    return function (..._args) {
        context[fn](...args, ..._args);
        delete context[fn];   
    }
}

4.手写new

function myNew(fn, ...args) {
    var obj = Object.create(fn.prototype);
    var res = fn.apply(obj, args);
    return typeof res === 'object' ? res: obj;
}

5.手写Object.create

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

6.手写ES5继承

面试题如下,想让student继承person,先写下基本框架

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {

}

原型继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {

}

student.prototype = new person();

构造继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student( ) {
    person.call(this);
}

组合继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {
    person.call(this);
}

student.prototype = new person();

原型式继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {

}

student.prototype = Object.create(person.prototype);

寄生组合继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {
    person.call(this);
}

student.prototype = person.prototype;
// 或者 student.prototype = Object.create(person.prototype);

寄生组合优化继承

function person() {
    this.kind = "person";
}

person.prototype.eat = function (food) {
    console.log(this.name + " is eating " + food);
}

function student() {
    person.call(this);
}

student.prototype = person.prototype;
// 或者 student.prototype = Object.create(person.prototype);
student.prototype.constructor = student;

7.手动实现instanceof

function myInstanceof(target, origin) {
    let proto = target.__proto__;
    if (proto) {
        if (proto === origin.prototype) {
            return true;
        } else {
            return myInstanceof(proto, origin);
        }
    } else {
        return false;
    }
}

8.手写Array.isArray

使用toString实现Array.isArray

Array.myIsArray = function(obj) {
    return Object.prototype.toString.call(Object(obj)) === '[object Array]';
}
console.log(Array.myIsArray([])); // true

使用instanceof实现Array.isArray

Array.myIsArray = function(obj) {
    return obj instanceof Array;
}
console.log(Array.myIsArray([])); // true

使用constructor实现Array.isArray

Array.myIsArray = function(obj) {
    return obj.constructor === Array;
}
console.log(Array.myIsArray([])); // true

9.实现一个函数判断数据类型

function getType(obj) {
    if (obj === null) return String(obj);
    // 对象类型 "[object XXX]"->XXX的小写 简单类型typeof obj
    return typeof obj === 'object' ? Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase() : typeof obj;
}

// 调用
console.log(getType(null)); // -> null
console.log(getType(undefined)); // -> undefined
console.log(getType({})); // -> object
console.log(getType([])); // -> array
console.log(getType(123)); // -> number
console.log(getType(true)); // -> boolean
console.log(getType('123')); // -> string
console.log(getType(/123/)); // -> regexp
console.log(getType(new Date())); // -> date

10.手写深拷贝

极简版

JSON.parse(JSON.stringify(obj))

该方法的局限性:

  • 无法实现对函数 、RegExp等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • 对象有循环引用,会报错
  • 所有以 symbol 为属性键的属性都会被完全忽略掉
  • 无法区分布尔值、数字、字符串及其包装对象
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

递归法

考虑到数组和对象

function deepCopy(obj) {
    let res;
    // 判断是否是引用类型,特别注意typeof null === "object"
    if (typeof obj === "object" && obj !== null) {
        // 复杂数据类型的类型
        res = obj.constructor === Array ? [] : {};
        for (let i in obj) {
            // 遍历对象中的每个元素是否为对象类型
            res[i] = typeof obj[i] === "object" ? deepCopy(obj[i]) : obj[i];
        }
    } else {
        // 简单数据类型 直接 = 赋值
        res = obj;
    }
    return res;
}

循环引用

上述版本执行下面这样一个测试用例:

function deepCopy(obj) {
    let res;
    // 判断是否是引用类型,特别注意typeof null === "object"
    if (typeof obj === "object" && obj !== null) {
        // 复杂数据类型的类型
        res = obj.constructor === Array ? [] : {};
        for (let i in obj) {
            // 遍历对象中的每个元素是否为对象类型
            res[i] = typeof obj[i] === "object" ? deepCopy(obj[i]) : obj[i];
        }
    } else {
        // 简单数据类型 直接 = 赋值
        res = obj;
    }
    return res;
}

let a = {
    val: 2
};
a.target = a;
let res = deepCopy(a);
console.log(res.target);
/* VM948:12 Uncaught RangeError: Maximum call stack size exceeded
    ....
*/

因为递归进入死循环导致栈内存溢出了。

img

原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况:

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储 key-value形式的数据,且 key可以是一个引用类型,我们可以选择 Map这种数据结构:

  • 检查map中有无克隆过的对象
  • 有 - 直接返回
  • 没有 - 将当前对象作为key,克隆对象作为value进行存储
  • 继续克隆
function deepCopy(obj, map = new Map()) {
    let res;
    // 判断是否是引用类型,特别注意typeof null === "object"
    if (typeof obj === "object" && obj !== null) {
        // 复杂数据类型的类型
        res = obj.constructor === Array ? [] : {};
        // map中有克隆过的对象,直接返回
        if (map.get(obj)) {
            return obj;
        }
        // map中没有克隆过的对象,进行存储
        map.set(obj, res);
        for (let i in obj) {
            // 遍历对象中的每个元素是否为对象类型
            res[i] = typeof obj[i] === "object" ? deepCopy(obj[i], map) : obj[i];
        }
    } else {
        // 简单数据类型 直接 = 赋值
        res = obj;
    }
    return res;
}

// 测试
let a = {
    val: 2
};
a.target = a;
let res = deepCopy(a);
console.log(res.target);

11.数组扁平化

多维数组=>一维数组

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

调用ES6中的flat方法

function flat(arr) {
    return arr.flat(Infinity);
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));
// [1, 2, 3, 4, 5, 6]

正则表达式

function flat(arr) {
    return JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));

使用JSON.parse还原为原先的格式

function flat(arr) {
    let str = '[' + JSON.stringify(arr).replace(/\[|\]/g, '').split(',') + ']';
    return JSON.parse(str);
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));
// [1, 2, 3, 4, null, 5, 6]

递归

function flat(arr) {
    let res = [];
    for (let item of arr) {
        if (item.constructor === Array) {
            res.push(...flat(item));
        } else {
            res.push(item);
        }
    }
    return res;
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));

ES5 对空位的处理,非常不一致,大多数情况下会忽略空位。

  • forEach(), filter(), reduce(), every()some() 都会跳过空位。
  • map() 会跳过空位,但会保留这个值。
  • join()toString() 会将空位视为 undefined,而undefinednull 会被处理成空字符串。

ES6 明确将空位转为 undefined

  • entries()keys()values()find()findIndex() 会将空位处理成 undefined
  • for...of 循环会遍历空位。
  • fill() 会将空位视为正常的数组位置。
  • copyWithin() 会连空位一起拷贝。
  • 扩展运算符(...)也会将空位转为 undefined
  • Array.from 方***将数组的空位,转为 undefined

使用forEach实现数组遍历可破

function flat(arr) {
    let res = [];
    arr.forEach(function (item){
        if (item.constructor == Array) {
        // if (instanceof(item) == Array) {
            res.push(...flat(item));
        } else {
            res.push(item);
        }
    })
    return res;
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));
// [1, 2, 3, 4, 5, 6]

利用reduce函数迭代(原地展开)

使用reduce实现

function flat(arr) {
    return arr.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
    }, []);
}
let arr = [1, [2, [3, [4, ,5]]], 6];
console.log(flat(arr));
// [1, 2, 3, 4, 5, 6]

扩展运算符

// 只要有一个元素有数组,那么循环继续
let arr = [1, [2, [3, [4, ,5]]], 6];
while (arr.some(Array.isArray)) {
    arr = [].concat(...arr);
}
console.log(arr);
//  [1, 2, 3, 4, empty, 5, 6]

12.数组去重

双层 for 循环

function unique(arr) {
    for (let i = 0, len = arr.length; i < len; i++) {
        for (let j = i + 1; j < len; j++) {
            // 防止出现类型转换 如果是==NaN只有一个,null消失3
            if (arr[i] === arr[j]) {
                // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
                arr.splice(j, 1);
                len--;
                j--;
            }
        }
    }
    return arr;
}

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] // NaN、{}没有去重

利用indexOf或者includes去重

> 新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组Object

使用indexOf判断

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

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  // NaN、{}没有去重

使用includes判断

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

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]  // {}没有去重

利用ES6 Set去重

function unique (arr) {
    return Array.from(new Set(arr));
}

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] // {}没有去重

利用展开运算符简写

function unique (arr) {
    return  [...new Set(arr)];
}

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] // {}没有去重

利用hasOwnProperty

> 利用hasOwnProperty 判断是否存在对象属性这种方法是利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值。 1 和 '1' 是不同的,但是这种直接作为key会判断为同一个值,这是因为对象的键值只能是字符串,所以我们可以使用 typeof item + item 拼成字符串作为 key 值来避免这个问题:

使用filter实现

function unique(arr) {
    var obj = {};
    return arr.filter(function(item){
        return obj,hasOwnProperty(typeof item + item) ? false : (obj[typeof iten + item] = true);
    })
}

let arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}] // NaN和{}去重

13.手写数组ES5常见方法

参数说明

  • callback 回调函数
  • context 执行 callback时使用的 this 值
  • current 数组中正在处理的元素
  • index 当前索引
  • array 源数组
  • accumulator 累加器
  • initialValue reduce或者reduceRight 第一次调用 callbackFn 函数时的第一个参数的值默认值
  • self 自己实现的 this 对象

1.forEach 函数

语法: arr.forEach(callback(current [, index [, array]])[, context])

方法功能:回调参数为:每一项、索引、原数组, 对数组的每个元素执行一次给定的函数。

返回: undefined。

自定义函数:myForEach。

Array.prototype.myForEach = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && element.length || 0;
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        callback.call(context, self[index], index, self);
    }
};

2.filter 函数

语法: var newArray = arr.filter(callback(current[, index[, array]])[, context])

方法功能: 创建一个新数组, 过滤掉回调函数返回值不为true的项,其包含通过所提供函数实现的测试的所有元素。

返回: 一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

自定义函数:myFilter。

Array.prototype.myFilter = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0,
        newArray = [];
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        if (callback.call(context, self[index], index, self)) newArray.push(self[index]);
    }
    return newArray;
};

3.find 函数

语法:arr.find(callback[, context])

方法功能: 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

返回: 数组中第一个满足所提供测试函数的元素的值,否则返回 undefined。

自定义函数:myFind。

Array.prototype.myFind = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0;
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        if (callback.call(context, self[index], index, self)) {
            return self[index];
        }
    }
    return undefined;
}

4.findIndex 函数

语法: arr.findIndex(callback[, context])

方法功能: 返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1。

返回: 数组中通过提供测试函数的第一个元素的索引。否则,返回-1。

自定义函数:myFindIndex。

Array.prototype.myFindIndex = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0;
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        if (callback.call(context, self[index], index, self)) return index;
    }
    return -1;
}

5.fill函数

语法: arr.fill(value[, start[, end]])

方法功能: 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

返回: 返回替换的值,原数组发生改变。

自定义函数:myFill。

Array.prototype.myFill = function(value, start = 0, end) {
    let self = this,
        len = self && self.length || 0;
    end = end || len;
    let loopStart = start < 0 ? 0 : start, // 设置循环开始值
        loopEnd = end >= len ? len : end; // 设置循环结束值

    for (; loopStart < loopEnd; loopStart++) {
        self[loopStart] = value;
    }
    return self;
}

6.map 函数

语法: var newArray = arr.map(function callback(current[, index[, array]]) {// Return self for newArray }[, context])

方法功能: 创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

返回: 测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。 一个由原数组每个元素执行回调函数的结果组成的新数组。

自定义函数:myMap。

Array.prototype.myMap = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0,
        result = [];
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        result[index] = callback.call(context, self[index], index, self);
    }
    return result;
}

7.some 函数

语法: arr.some(callback(current[, index[, array]])[, context])

方法功能: 测试数组中是不是至少有1个元素通过了被提供的函数测试,回调函数返回值一个为true 结果就为true, 否则为false。它返回的是一个Boolean类型的值。

返回: 数组中有至少一个元素通过回调函数的测试就会返回true;所有元素都没有通过回调函数的测试返回值才会为false。

自定义函数:mySome。

Array.prototype.mySome = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0;
    if (!context) context = self;
    for (let index = 0; index < len; index++) {
        if (callback.call(context, self[index], index, self)) return true;
    }
    return false;
}

8.every 函数

语法: arr.every(callback(current[, index[, array]])[, context])

方法功能:测试一个数组内的所有元素是否都能通过某个指定函数的测试,所有回调函数返回值都为true时 结果为true,否则为false。它返回一个布尔值。

返回: 如果回调函数的每一次返回都为 true 值,返回 true,否则返回 false。

自定义函数:myEvery。

Array.prototype.myEvery = function(callback, context) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self && self.length || 0;
    if (!context) context = self;
    for(let index = 0; index < len; index++) {
        if (!callback.call(context, element[index], index, element)) return false;
    }
    return true;
}

9.reduce 函数

语法:arr.reduce(callback(accumulator, current[, index[, array]])[, initialValue])

方法功能:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。相比其他方法多了一个参数即上次调用的返回值,
最后一个回调函数的返回值为reduce的结果,可以指定累积的初始值,不指定初始值从第二项开始遍历

返回: 函数累计处理的结果。

自定义函数:myReduce。

Array.prototype.myReduce = function(callback, initialValue) {
    if (typeof callback !== 'function') throw ('callback参数必须是函数');
    let self = this,
        len = self.length || 0;
    let result = initialValue ? initialValue : self[0]; // 不传默认取数组第一项
      let index = initialValue ? 0 : 1;

    while (index < len) {
        if (index in self) result = callback(result, self[index], index, self);
        index++;
    }
    return result;
}

14.实现数组原地反转

用了双指针,第三变量交换法

function revert(arr, start, end) {
    while (start < end) {
        let temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
        start++;
        end--;
    }
}

let arr = [0, 1, 4, 9, 16, 25];
revert(arr, 2, 5);
console.log(arr);
// [0, 1, 25, 16, 9, 4]

解构赋值,

function revert(arr, start, end) {
    while (start < end) {
        [arr[start], arr[end]] = [arr[end], arr[start]];
        start++;
        end--;
    }
}

let arr = [0, 1, 4, 9, 16, 25];
revert(arr, 2, 5);
console.log(arr);
// [0, 1, 25, 16, 9, 4]

利用和或者位运算

function revert(arr, start, end) {
    while (start < end) {
        arr[start] += arr[end];
        arr[end] = arr[start] - arr[end];
        arr[start] = arr[start] - arr[end];
        start++;
        end--;
    }
}

let arr = [0, 1, 4, 9, 16, 25];
revert(arr, 2, 5);
console.log(arr);
// [0, 1, 25, 16, 9, 4]

或者

function revert(arr, start, end) {
    while (start < end) {
        arr[start] ^= arr[end];
        arr[end] ^= arr[start];
        arr[start] ^= arr[end];
        start++;
        end--;
    }
}

let arr = [0, 1, 4, 9, 16, 25];
revert(arr, 2, 5);
console.log(arr);

15.reduce的应用汇总

reduce语法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue);
/*
  total: 必需。初始值, 或者计算结束后的返回值。
  currentValue: 必需。当前元素。
  currentIndex: 可选。当前元素的索引;                     
  arr: 可选。当前元素所属的数组对象。
  initialValue: 可选。传递给函数的初始值,相当于total的初始值。
*/

> reduceRight() ,该方法用法与reduce()其实是相同的,只是遍历的顺序相反,它是从数组的最后一项开始,向前遍历到第一项

数组求和

基础版本

const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num);
console.log(sum); // 69

设定初始值求和

const arr = [12, 34, 23];
const sum = arr.reduce((total, num) => total + num, 10);  // 以10为初始值求和
console.log(sum); // 79

数组最大值

const arr = [23, 123, 342, 12];
const max = arr.reduce((pre, cur) => pre > cur ? pre : cur, Number.MIN_SAFE_INTEGER);
console.log(max); // 342

数组转对象

var streams = [{name: '技术', id: 1}, {name: '设计', id: 2}];
var obj = streams.reduce((accumulator, cur) => {accumulator[cur.id] = cur; return accumulator;}, {});

数组扁平化

数组扁平化

数组去重

数组去重 加 indexOf/includes)

求字符串中字母出现的次数

const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha';

const res = str.split('').reduce((count, next) => {
    count[next] ? count[next]++ : count[next] = 1;
    return count;
},{});
console.log(res);
// 结果
/*
    -: 1
    a: 8
    c: 1
    d: 4
    e: 1
    f: 4
    g: 1
    h: 2
    i: 2
    j: 4
    k: 1
    l: 3
    m: 2
    n: 1
    q: 5
    r: 1
    s: 6
    u: 1
    w: 2
    x: 1
    z: 1
*/

compose函数

> redux compose 源码实现

function compose(...funs) {
    if (funs.length === 0) {
        return arg => arg;
    }
    if (funs.length === 1) {
       return funs[0];
    }
    return funs.reduce((a, b) => (...arg) => a(b(...arg)))
}

const partial = (fn, ...args) => (..._args) =>
    fn(...args, ..._args);

const partialRight = (fn, ...args) => (..._args) =>
    fn(..._args, ...args);

function add(x, y) {
    return x + y;
}

function pow(x, y) {
      return Math.pow(x, y);
}

function double(x) {
      return x * 2;
}

function multiply(x, y) {
    return x * y;
}

compose(
    console.log, 
    partial(add, 10),
    partialRight(pow, 3),
    partial(multiply, 5)
)(2); // 1010

或者使用reduceRight

function compose(...funs) {
    if (funs.length === 0) {
        return arg => arg;
    }
    if (funs.length === 1) {
       return funs[0];
    }
    return funs.reduceRight((a, b) => (...arg) => b(a(...arg)))
}

const partial = (fn, ...args) => (..._args) =>
    fn(...args, ..._args);

const partialRight = (fn, ...args) => (..._args) =>
    fn(..._args, ...args);

function add(x, y) {
    return x + y;
}

function pow(x, y) {
      return Math.pow(x, y);
}

function double(x) {
      return x * 2;
}

function multiply(x, y) {
    return x * y;
}

compose(
    console.log, 
    partial(add, 10),
    partialRight(pow, 3),
    partial(multiply, 5)
)(2); // 1010

更多写法请参考手写compose函数

实现多维数组的回溯

实现[['a', 'b'], ['n', 'm'], ['0', '1']] => ["an0", "an1", "am0", "am1", "bn0", "bn1", "bm0", "bm1"]

function backtrack(arr) {
    return arr.reduce((prev, cur) => {
        let list = [];
        for (let i = 0; i < prev.length; i++) {
            for (let j = 0; j < cur.length; j++) {
                list.push(prev[i] + cur[j]);
            }
        }
        return list;
    })
}

console.log(backtrack([['a', 'b'], ['n', 'm'], ['0', '1']]));
// ["an0", "an1", "am0", "am1", "bn0", "bn1", "bm0", "bm1"]

16.洗牌算法

最简单的一种形式,遍历的时候进行交换

function shuffle(array) {
    const length = array.length;
    for (let i = 0; i < length; i++) {
        let random = Math.floor(length * Math.random());
        [array[i], array[random]] = [array[random], array[i]];
    }
}
let arr = Array.from(Array(100), (item, index) => index);
shuffle(arr);
console.log(arr);

公认成熟的洗牌算法(Fisher-Yates),简单的思路如下:

    1. 定义一个数组,以数组的最后一个元素为基准点。
    1. 在数组开始位置到基准点之间随机取一个位置,将所取位置上的元素和基准点上的元素互换。
    1. 基准点左移一位。
    1. 重复2,3步骤,直到基准点为数组的开始位置。
function shuffle(arr) {
    let length = arr.length;
    for (let i = length - 1; i >= 0; i--) {
        let random = Math.floor(Math.random() * (i + 1)); // 生成起始位置到基准位置之间的随机位置,并将基准从结束位置不停左移。
        // es3实现
        // var newA = arr[i];
        // arr[i] = arr[random];
        // arr[random] = newA;
        // es6 实现
        [arr[i], arr[random]] = [arr[random], arr[i]]; // 本质为交换元素位置。
    }
    return arr;
}
let arr = Array.from(Array(100), (item, index) => index);
shuffle(arr);
console.log(arr);

17.对象扁平化

实现一个 objectFlat 函数,实现如下的转换功能

const obj = {
    a: 1,
    b: [1, 2, { c: true }],
    c: { e: 2, f: 3 },
    g: null,
};
// 转换为
let objRes = {
    a: 1,
    "b[0]": 1,
    "b[1]": 2,
    "b[2].c": true,
    "c.e": 2,
    "c.f": 3,
    g: null,
};

我们从结果入手,可以知道我们需要对象进行遍历,把里面的属性值依次输出,所以我们可以知道核心方法体就是:传入对象的 key 值和 value,对 value 再进行递归遍历。

我们知道 js 的数据类型可以基础数据类型和引用数据类型,对于题目而言,基础数据类型无需再进行深层次遍历,引用数据类型需要再次进行递归。

function objectFlat(obj = {}) {
    const res = {};
    function flat(value, key = '') {
        // 首先判断是基础数据类型还是引用数据类型
        if (typeof value !== "object" || value === null) {
            // 基础数据类型
            if (key) {
                res[key] = value;
            }
        } else if (Array.isArray(value)) { // 判断是数组
            for (let i = 0; i < value.length; i++) {
                flat(value[i], key + `[${i}]`);
            }
        } else { // 判断是对象
            let keys = Object.keys(value);
            keys.forEach(item => {
                flat(value[item], key ? `${key}.${item}` : `${item}`);
            })
            // 空对象
            if (!keys.length && key) {
                res[key] = {};
            }
        }
    }
    flat(obj);
    return res;
}

// 测试
const source = {
    a: {
        b: [
            1,
            2,
            {
                c: 1,
                d: 2
            }
        ],
        e: 3
    },
    f: {
        g: 2
    }
};
console.log(objectFlat(source));
/*
    a.b[0]: 1
    a.b[1]: 2
    a.b[2].c: 1
    a.b[2].d: 2
    a.e: 3
    f.g: 2
*/

18.手写偏函数

一天在面试中,面试官给了我一道手写代码题

/**
    * 实现函数 partialUsingArguments,调用之后满足如下条件:
    1、返回一个函数 result
    2、调用 result 之后,返回的结果与调用函数 fn 的结果一致
    3、fn 的调用参数为 partialUsingArguments 的第一个参数之后的全部参数以及 result 的调用参数
*/ 

我当时的第一版思路,将两个参数数组进行拼接,通过闭包返回结果,面试官提示如果参数为空,怎么办,我增加了args = args || [];这一句

function partialUsingArguments(fn, ...args) {
    args = args || [];
    return function (..._args) {
        return fn(args.concat(_args));
    }
}

面试官说如果参数不是数组,是对象怎么办,提示ES6还有什么拼接方法,使用展开运算符

常规写法

function partialUsingArguments(fn, args) {
    return function (_args) {
        return fn(...args, ..._args);
    }
}

简化写法

const partialUsingArguments = (fn, ...args) => (..._args) =>
    fn(...args, ..._args);

19.函数柯里化

定义

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

通俗易懂的解释:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数。

常规写法

function curry(fn, ...args) {
    if (args.length >= fn.length) {
        // 判断当前函数传入的参数是否大于或等于fn需要参数的数量,如果是,直接执行fn
        return fn(...args);
    } else {
        // 如果传入参数数量不够,返回一个闭包,暂存传入的参数,并重新返回curry函数
        return (..._args) => curry(fn, ...args, ..._args);
    }
}

function multiFn(a, b, c) {
    return a * b * c;
}

var multi = curry(multiFn);

console.log(multi(2)(3)(4)); // 24
console.log(multi(3, 4, 5)); // 60
console.log(multi(4)(5, 6)); // 120
console.log(multi(5, 6)(7)); // 210

简化写法

const curry = (fn, arr = []) => (..._args) => (
    args => args.length === fn.length ? fn(...args) : curry(fn, args)
)([...arr, ..._args])

function multiFn(a, b, c) {
    return a * b * c;
}

var multi = curry(multiFn);

console.log(multi(2)(3)(4)); // 24
console.log(multi(3, 4, 5)); // 60
console.log(multi(4)(5, 6)); // 120
console.log(multi(5, 6)(7)); // 210

20.手写compose函数

基于栈的compose函数

function compose(...args) {
    return function(result) {
        const funcs = [...args];
        while(funcs.length > 0) {
            result = funcs.pop()(result);
        }
        return result;
    };
}

// import { partial, partialRight } from 'lodash';
const partial = (fn, ...args) => (..._args) =>
    fn(...args, ..._args);

const partialRight = (fn, ...args) => (..._args) =>
    fn(..._args, ...args);

function add(x, y) {
    return x + y;
}

function pow(x, y) {
      return Math.pow(x, y);
}

function double(x) {
      return x * 2;
}

const add10 = partial(add, 10);
const pow3 = partialRight(pow, 3);
compose(console.log, add10, pow3, double)(2) // 74

使用函数reduce的compose函数

/* function compose(...funcs) {
    return funcs
        .reverse()
        .reduce((fn1, fn2) => (...args) => fn2(fn1(...args)));
} */

function compose(...funcs) {
    return funcs
        .reduce((fn1, fn2) => (...args) => fn1(fn2(...args)));
}

// import { partial, partialRight } from 'lodash';
const partial = (fn, ...args) => (..._args) =>
    fn(...args, ..._args);

const partialRight = (fn, ...args) => (..._args) =>
    fn(..._args, ...args);

function add(x, y) {
    return x + y;
}

function pow(x, y) {
      return Math.pow(x, y);
}

function double(x) {
      return x * 2;
}

const add10 = partial(add, 10);
const pow3 = partialRight(pow, 3);
compose(console.log, add10, pow3, double)(2) // 74

21.实现 (5).add(3).minus(2) 功能

> 例: 5 + 3 - 2,结果为 6

Number.prototype.add = function(n) {
    return this.valueOf() + n;
}
Number.prototype.minus = function(n) {
    return this.valueOf() + n;
}
console.log((5).add(3).minus(2)); // 6

22.实现一个 add 函数

满足以下功能

add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6

需要结合上述的偏函数和toString()方法实现功能,打印函数时会自动调用 toString()方法

function add(...args) {
    let fn = function(..._args) {
        return add(...args, ..._args);
    }

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

    return fn;
}
console.log(add(1)); // 1
console.log(add(1)(2)); // 3
console.log(add(1)(2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1, 2, 3)); // 6

23.计算两个数组的交集

> 例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]。

排序+双指针

function union (nums1, nums2) {
    nums1.sort((x, y) => x - y);
    nums2.sort((x, y) => x - y);
    const length1 = nums1.length, length2 = nums2.length;
    // 双指针
    let index1 = 0, index2 = 0;
    const intersection = [];
    while (index1 < length1 && index2 < length2) {
        const num1 = nums1[index1], num2 = nums2[index2];
        if (num1 === num2) {
            intersection.push(num1);
            index1++;
            index2++;
        } else if (num1 < num2) {
            index1++;
        } else {
            index2++;
        }
    }
    return intersection;
};

 const a = [1, 2, 2, 1];
 const b = [2, 3, 2];
 console.log(union(a, b)); // [2, 2]

24.手写对象深度比较

> 思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归

  • 递归退出条件:
    • 被比较的是两个值类型变量,直接用“===”判断
    • 被比较的两个变量之一为null,直接判断另一个元素是否也为null
  • 提前结束递推:
    • 两个变量keys数量不同
    • 传入的两个参数是同一个变量
  • 递推工作:  - 深度比较每一个key
function isEqual(obj1, obj2){
    // 其中一个为值类型或null
    if (!isObject(obj1) || !isObject(obj2)) return obj1 === obj2;

    // 判断是否两个对象是同一个变量
    if(obj1 === obj2) return true;

    // 判断keys数是否相等
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length) return false;

    // 深度比较每一个key
    for(let key of obj1Keys){
        // 递归查询
        if (!isEqual(obj1[key], obj2[key])) return false;
    }

    return true;
}

25.扁平数组转树状结构

怎么进行格式转换,将data转换成result形式(手写代码)

const data = [
    { id: 10, parentId: 0, text: "一级菜单-1" }, 
    { id: 20, parentId: 0, text: "一级菜单-2" },
    { id: 30, parentId: 20, text: "二级菜单-3" },
    { id: 25, parentId: 30, text: "三级菜单-25" },
    { id: 35, parentId: 30, text: "三级菜单-35" }
];

let result = [
  {
    id: 10,
    text: '一级菜单-1',
    parentId: 0
  },
  {
    id: 20,
    text: '一级菜单-2',
    parentId: 0,
    children: [
      {
        id: 10,
        text: '一级菜单-3',
        parentId: 20,
        children: [...]
      }
    ]
  }
];

一开始以为只有一层子节点,打算先把有子节点的放入,再遍历有父节点的,写了一半重新理了理思路,写了以下的代码,先根据id从小到大排序,反着遍历,将子节点塞进父节点的children数组中

const data = [
    { id: 10, parentId: 0, text: "一级菜单-1" }, 
    { id: 20, parentId: 0, text: "一级菜单-2" },
    { id: 30, parentId: 20, text: "二级菜单-3" },
    { id: 25, parentId: 30, text: "三级菜单-25" },
    { id: 35, parentId: 30, text: "三级菜单-35" }
];
function convert(data) {
    data.sort((a, b) => a.parentId - b.parentId);
    for (let i = data.length - 1; i>= 0; i--) {
        if (data[i].parentId === 0) break;
        for (let j = i - 1; j >= 0; j--) {
            if (findParent(i, j)) {
                break;
            }
        }
    }
    return data;
    function findParent(a, b) {
        if (data[a].parentId == data[b].id) {
            data[b].children = data[b].children || [];
            data[b].children.push(data.splice(a, 1));
            return true;
        }
        return false;
    }
}
console.log(convert(data));

想要多转换方法,可以参考JS树形结构处理

26.防抖(debounce)

不管事件触发频率多高,一定在事件触发n秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,就以新的事件的时间为准,n秒后才执行,总之,触发完事件 n 秒内不再触发事件,n秒后再执行。

适用场景:

> 按钮提交场景:防止多次提交按钮,只执行最后提交的一次 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

防抖代码(第一版)

根据这段表述,我们可以写第一版的代码:

// 第一版
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

this指向(第二版)

但是如果使用第一版 debounce 函数,this 就会指向 Window 对象,而非对应的元素!所以我们需要将 this 指向正确的对象。

我们修改下代码:

// 第二版
// func是用户传入需要防抖的函数
// wait是等待时间
function debounce(func, wait) {
    // 缓存一个定时器id
    let timer = 0;
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function () {
        let context = this;
        if (timer) clearTimeout(timer);
        timer = setTimeout(function(){
            func.apply(context);
        }, wait);
    }
}
// 利用箭头函数改变this指向
function debounce(func, wait = 50) {
    // 缓存一个定时器id
    let timer = 0;
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function() {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this);
        }, wait)
    }
}

现在 this 已经可以正确指向了

正确传参(第三版,已经完成基本功能)

上述代码无法实现含有参数的函数的防抖,需要进行改进

所以我们再修改一下代码:

// 第三版
// func是用户传入需要防抖的函数
// wait是等待时间
function debounce(func, wait = 50) {
    // 缓存一个定时器id
    let timer = 0;
    // 这里返回的函数是每次用户实际调用的防抖函数
    // 如果已经设定过定时器了就清空上一次的定时器
    // 开始一个新的定时器,延迟执行用户传入的方法
    return function(...args) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args);
        }, wait)
    }
}

功能更丰富的防抖函数请参考JavaScript专题之跟着underscore学防抖

27.节流(throttle)

节流是什么

节流规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次,实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器。规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

适用场景:

> - 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
> - 缩放场景:监控浏览器resize
> - 动画场景:避免短时间内多次触发动画引起性能问题

时间戳版代码

让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

// 第一版
// func是用户传入需要防抖的函数
// wait是等待时间
function throttle (func, wait = 50) {
    // 上一次执行该函数的时间
    let lastTime = 0;
    return function (...args) {
        // 当前时间
        let now = +new Date();
        // 将当前时间和上一次执行函数时间对比
        // 如果差值大于设置的等待时间就执行函数
        if (now - lastTime > wait) {
            lastTime = now;
            func.apply(this, args);
        }
    }
}

// 使用方法:定时器
setInterval(
    throttle(() => {
        console.log(1)
    }, 500),
    1
);

定时器版代码

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

// 第二版
// func是用户传入需要防抖的函数
// wait是等待时间
function throttle(func, wait = 50) {
    // 上一次执行该函数的时间
    let timer = null;
    return function(...args) {
        let context = this;
        if (!timer) {
            timer = setTimeout(function() {
                timer = null;
                func.apply(context, args);
            }, wait)
        }
    }
}
// 或者采用箭头函数
function throttle(func, wait = 50) {
    // 上一次执行该函数的时间
    let timer = null;
    return function(...args) {
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
                func.apply(context, args);
            }, wait)
        }
    }
}
// 使用方法:定时器
setInterval(
    throttle(() => {
        console.log(1)
    }, 500),
    1
)

使用两个时间戳prev旧时间戳now新时间戳,每次触发事件都判断二者的时间差,如果到达规定时间,执行函数并重置旧时间戳
比较两个方法:

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

功能更丰富的节流函数请参考JavaScript专题之跟着 underscore 学节流

28.手写const

由于ES5环境没有block的概念,所以是无法百分百实现const,只能是挂载到某个对象下,要么是全局的window,要么就是自定义一个object来当容器对于const不可修改的特性,我们通过设置writable属性来实现

var _const = function __const (data, value) {
    // 把要定义的data挂载到window下,并赋值value
    window.data = value;
    // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
    Object.defineProperty(window, data, {
        // 不可枚举
        enumerable: false,
        // 不可删除
        configurable: false,
        get: function() {
            // 返回值
            return value;
        },
        set: function(data) {
            if (data !== value) {
                // 当要对当前属性进行重新赋值时,则抛出错误!
                throw new TypeError('Assignment to constant variable.');
            } else {
                return value;
            }
        }

    })
}
// 测试
// 定义a
_const('a', 10);
console.log(a); // 10
delete a; // false
console.log(a); // 10
// 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
for (let item in window) { 
    if (item === 'a') {
        // 因为不可枚举,所以不执行
        console.log(window[item]);
    }
}
a = 20; // 报错
// 定义obj
_const('obj', {a: 1});
console.log(obj);
// 可以正常给obj的属性赋值
obj.b = 2;
console.log(obj);
obj = {}; / 无法赋值新对象 报错

29.实现一个双向绑定

defineProperty 版本

利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在vue3.0中通过Proxy代理对象进行类似的操作。

// 数据
const data = {
    text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持 对象名称和属性名称
Object.defineProperty(data, 'text', {
    enumerable: true,
    configurable: true,
    // 数据变化 --> 修改视图
    set(value) {
        input.value = value,
        span.innerHTML = value;
        return value;
    }
})
// 数据变化 --> 修改视图
input.addEventListener('keyup', function(e) {
    data.text = e.target.value;
})

proxy 版本

Object.defineProperty() 的问题主要有三个:

  • 不能监听数组的变化

  • 必须遍历对象的每个属性

  • 必须深层遍历嵌套的对象

    Proxy 在 ES2015 规范中被正式加入,它有以下几个优势点

  • 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 第二个问题

  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。

  • Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富

  • Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法,可以享受新版本红利。

// 数据
const data = {
    text: 'default'
};
const input  = document.getElementById('input');
const span  = document.getElementById('span');
// 数据劫持 对象名称和文本名称
const handler = {
    set(target, key, vlaue) {
        // target = 目标对象
        // prop = 设置的属性
        // value = 修改后的值
        target[key] = value;
        // 数据变化 --> 修改视图
        input.value = value;
        span.innerHTML = value;
        return value;
    }
} 
// 实现代理
const proxy = new Proxy(data, handler);

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

30.图片懒加载

监听scroll事件法

图片,用一个其他属性存储真正的图片地址:

  <img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/09/09/16/05/forest-931706_1280.jpg" alt="">
  <img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/08/01/00/08/pier-407252_1280.jpg" alt="">
  <img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2014/12/15/17/16/pier-569314_1280.jpg" alt="">
  <img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2010/12/13/10/09/abstract-2384_1280.jpg" alt="">
  <img src="loading.gif" data-src="https://cdn.pixabay.com/photo/2015/10/24/11/09/drop-of-water-1004250_1280.jpg" ``` 通过图片`offsetTop`和`window`的`innerHeight`,`scrollTop`判断图片是否位于可视区域。 ```js 节流函数,保证每200ms触发一次 function throttle(func, wait="200)" { let timer="setTimeout(()" return (...args) if (!timer)> {
                timer = null;
                func.apply(this, args);
            }, wait);
        }
    }
}

// 获取所有img标签
var imgs = document.getElementsByTagName("img");
// 存储图片已经实现加载的位置,避免每次都从第一张图片开始遍历
var n = 0;
// 页面载入完毕加载可是区域内的图片
lazyload();

// 监听页面滚动事件
window.addEventListener('scroll', throttle(lazyload, 200));
// 懒加载函数
function lazyload() {
    // 可见区域高度
    let visualHeight = window.innerHeight;
    // 滚动条距离顶部高度,注意要兼容IE浏览器
    let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
    // 从上一个没有加载完毕的img便利到最后的img
    for (let i = n; i &lt; imgs.length; i++) {
        // 在视窗范围以内
        if (img[i].offsetTop &lt; visualHeight + scrollTop) {
            if (img[i].getAttribute('src') === "loading.gif") {
                // 将src替换为data-src
                img[i].src = img[i].getAttribute("data-src");
            }
            n = i + 1;
        }
    }
}

IntersectionObserver

> IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

Intersection Observer可以不用监听scroll事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见。

// 获取所有img标签
var imgs = document.getElementsByTagName("img");

// 判断IntersectionObserver可用
if (IntersectionObserver) {
    let lazyloadObserver = new IntersectionObserver((entries) =&gt; {
        entries.forEach((entry) =&gt; {

        })
    })
    let lazyImgObserver = new IntersectionObserver((entries, observer) =&gt; {
        entries.forEach((entry, index) =&gt; {
            // 懒加载图片
            let lazyImg = entry.target;
            // 如果元素可见
            if (entry.intersectionRatio &gt; 0) {
                if (lazyImg.getAttribute("src") === "loading.gif") {
                    lazyImg.src = lazyImg.getAttribute("data-src");
                }
                // 图片加载后即停止监听该元素
                lazyImgObserver.unobserve(lazyImg);
            }
        })
    })
    // observe遍历监听所有img节点
    for (let i = 0; i &lt; imgs.length; i++) {
        lazyImgObserver.observe(imgs[i]);
    }
}

31.区间随机数生成器

function random(m, n) {
    return Math.floor(Math.random() * (n - m)) + m;
    // return parseInt(Math.random() * (n - m)) + m;
}

for (let i = 0; i &lt; 10; i++) {
    console.log(random(28, 45));
}

32.打印菱形

function printDiamond(n) {
    for (let i = 0; i &lt; n; i++) {
        for (let j = 0; j &lt;= i; j++) {
            document.write("* ");
        }
        document.write("<br>");
    }
    for (let i = n - 2; i &gt;= 0; i--) {
        for (let j = 0; j &lt;= i; j++) {
            document.write("* ");
        }
        document.write("<br>");
    }
}

33.手写parseInt

一开始以为很简单,写着写着 发现好难,还是先写只带有数字、字母的,0x什么开头的我怕是头想秃了都不会

function getNum(char) {
    if ('0' &lt;= char &amp;&amp; char &lt;= '9') return Number(char);
    if ('a' &lt;= char &amp;&amp; char &lt;= 'z') return char.charCodeAt() - 'a'.charCodeAt() + 10;
    if ('A' &lt;= char &amp;&amp; char &lt;= 'Z') return char.charCodeAt() - 'A'.charCodeAt() + 10;
}
function _parseInt(str, radix) {
    // 字符串类型
    let strType = Object.prototype.toString.call(str);
    // 如果类型不是 string 或 number 类型返回NaN
    if (strType !== '[object String]' &amp;&amp; strType !== '[object Number]') return NaN;

    // 如果 radix 为0 null undefined
    if (!radix) {
        // 则转化为 10
        radix = 10;
    }

    if (Object.prototype.toString.call(radix) !== '[object Number]' || radix &lt; 2 || radix &gt; 36 || Math.floor(radix) &lt; radix){
        return NaN;
    }

    // 正则表达式,表示数
    const re = /^[\-|\+]?[0-9a-zA-Z]*(\.[0-9a-zA-Z]+)?/;
    // 字符串处理,把小数点以后的去除
    str = (str + '').trim().match(re)[0];
    if (!str.length) return NaN;
    let sign = "+";
    // 处理特殊情况
    if (str[0] === '+') {
        str = str.slice(1);
    }
    if (str[0] === '-') {
        sign = "-";
        str = str.slice(1);
    }
    if (str[0] === '.') {
        if (str[1]) {
            let num = getNum(str[1]);
            if (num &lt; radix &amp;&amp; sign === '+') return 0;
            if (num &lt; radix &amp;&amp; sign === '-') return -0;
            return NaN;
        }
        return NaN;
    }
    // 把小数点后面的去除
    str = str.split('.')[0];
    if (!str.length) return NaN;
    let res = getNum(str[0]);
    if (res &gt;= radix) return NaN;
    for (let i = 1; i &lt; str.length; i++) {
        let num = getNum(str[i]);
        if (num &gt;= radix) return sign === '+' ? res : -res;
        res = res * radix + num;
    }
    return sign === '+' ? res : -res;
}

console.log(_parseInt("F", 16));
console.log(_parseInt("17", 8));
console.log(_parseInt("015", 10));
console.log(_parseInt(15.99, 10));
console.log(_parseInt("15,123", 10));
console.log(_parseInt("FXX123", 16));
console.log(_parseInt("1111", 2));
console.log(_parseInt("15 * 3", 10));
console.log(_parseInt("15e2", 10));
console.log(_parseInt("15px", 10));
console.log(_parseInt("12", 13));
console.log('-------------------------------');
console.log(_parseInt("Hello", 8));
console.log(_parseInt("546", 2));
console.log('-------------------------------');
console.log(_parseInt("-F", 16));
console.log(_parseInt("-0F", 16));
console.log(_parseInt(-15.1, 10));
console.log(_parseInt(" -17", 8));
console.log(_parseInt(" -15", 10));
console.log(_parseInt("-1111", 2));
console.log(_parseInt("-15e1", 10));
console.log(_parseInt("-12", 13));

大佬的版本,可以参考下,让我当场写肯定不会

function compare(str, radix) {
    let code = str.toUpperCase().charCodeAt(0),
        num;
    if (radix &gt;= 11 &amp;&amp; radix &lt;= 36) {
        if (code &gt;= 65 &amp;&amp; code &lt;= 90) {
            num = code - 55;
        } else {
            num = code - 48;
        }
    } else {
        num = code - 48;
    }
    return num;
}

function isHex(first, str) {
    return first === '0' &amp;&amp; str[1].toUpperCase() === 'X'
}

function _parseInt(str, radix) {
    str = String(str);
    if (typeof str !== 'string') return NaN;
    str = str.trim();
    let first = str[0],
        sign;
    //处理第一个字符为 '-' || '+' 的情况
    if (first === '-' || first === '+') {
        sign = str[0];
        str = str.slice(1);
        first = str[0];
    }
    //当 radix 不存在或者小于 11 时,第一个字符只能为数字
    if (radix === undefined || radix &lt; 11) {
        if (isNaN(first)) return NaN;
    }

    let reg = /^(0+)/;
    //截取 str 前面符合要求的一段,直到遇到非数字和非字母的字符
    let reg2 = /^[0-9a-z]+/i;
    str = str.match(reg2)[0];
    let len = str.length;
    //在没有第二个参数时或者不是数字时,给第二个参数赋值
    //isNaN('0x12') 会执行 Number('0x12') 可以转换成十进制
    if (radix === undefined || isNaN(radix) || radix === 0) {
        if (len === 1) return str;
        //如果 str 是十六进制形式,就转换成十进制
        if (isHex(first, str)) {
            if (sign === '-') {
                return Number(-str);
            } else {
                return Number(str);
            }
        } else {
            //不能直接返回 Number(str) 比如 Number('0ff23') 会返回 NaN,但是应该返回 0
            radix = 10;
        }
    } else {
        //如果有第二个参数,并且是数字,要处理第二个参数
        radix = String(radix);
        //如果有小数点,取小数点前面一段,处理不为整数的情况
        radix = radix.split('.')[0];
        //如果 radix 前面有零将零去除,十六进制除外
        if (radix.length &gt; 1) {
            let twoR = radix[1].toUpperCase();
            if (radix[0] === '0' &amp;&amp; twoR !== 'X') radix = radix.replace(reg, '');
        }
        //如果 radix 是十六进制的字符串类型,也会转变成十进制的数字类型
        radix = Number(radix);
        //radix 是否在正确的区间
        if (radix &gt;= 2 &amp;&amp; radix &lt;= 36) {
            //如果 radix 为 16,且 str 是十六进制形式的话,直接将十六进制转换成十进制
            if (radix === 16 &amp;&amp; isHex(first, str)) return Number(str);
        } else {
            //只要 radix 是一个有效的数字,但不在正确的区间里,就返回 NaN
            return NaN;
        }
    }
    //去除 str 前面的零
    str = str.replace(reg, '');
    if (str.length === 0) return 0;
    let strArr = str.split(''),
        numArr = [],
        result = 0,
        num;
    for (let i = 0; i &lt; strArr.length; i++) {
        num = compare(strArr[i], radix);
        if (num &lt; radix) {
            numArr.push(num);
        } else {
            break;
        }
    }
    let lenN = numArr.length;
    if (lenN &gt; 0) {
        numArr.forEach(function (item, index) {
            result += item * Math.pow(radix, lenN - index - 1);
        });
    } else {
        //str 开头有零的话要返回零
        return first === '0' ? 0 : NaN;
    }
    if (sign === '-') result = -result;
    return result;
}

34.手写JSON.stringify

先熟悉JSON.stringify的用法

JSON.stringify(value[, replacer [, space]]):
  • Boolean | Number| String类型会自动转换成对应的原始值。
  • undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。
  • 不可枚举的属性会被忽略如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略
  • 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略

手写代码如下

function jsonStringify(obj) {
    let type = typeof obj;
    if (type !== 'object') {
        // 不是字符串 undefined 和 function 类型
        if (/string|undefined|function/.test(type)) {
            obj = '"' + obj + '"';
        }
        return String(obj);
    }
    // JSON为空数组
    let json = [];
    // 是否为数组
    let arr = Array.isArray(obj);
    for (let key in obj) {
        // 递归调用
        let value = jsonStringify(obj[key]);
        json.push((arr ? "" : '"' + key + '":') + String(value));
    }
    return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}

console.log(jsonStringify({x : 5})); // {"x":5}
console.log(jsonStringify([1, "false", false])); // [1,"false",false]
console.log(jsonStringify({b: undefined})); // {"b":"undefined"}

手写了一下,是不是对于JSON.stringify的不足之处又有了全新的理解了呢,再次强调如下:

  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方***抛出错误。
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  • Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。

    35.手写JSON.parse

    先熟悉JSON.parse的用法

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

直接调用 eval

function jsonParse(opt) {
    return eval('(' + opt + ')');
}

console.log(jsonParse(JSON.stringify({x : 5}))); // [object Object]: { x: 5}
console.log(jsonParse(JSON.stringify([1, "false", false]))); // [object Array]: [1, "false", false]
console.log(jsonParse(JSON.stringify({b: undefined}))); // [object Object]: {}

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

如果你只想记这个方法,就得对参数json做校验。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
    rx_one.test(
        json
            .replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {
    var obj = eval("(" +json + ")");
}

调用Function
核心:Function与eval有相同的字符串参数特性

var func = new Function(arg1, arg2, ..., functionBody);

在转换JSON的实际应用中,只需要这么做

var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();

eval 与 Function都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用

测试结果如下

let jsonStr = JSON.stringify({x : 5});
console.log((new Function('return ' + jsonStr))()); // [object Object]: {x: 5}
jsonStr = JSON.stringify([1, "false", false]);
console.log((new Function('return ' + jsonStr))()); // [object Array]: [1, "false", false]
jsonStr = JSON.stringify({b: undefined});
console.log((new Function('return ' + jsonStr))()); // [object Object]: {}

eval 与 Function 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。

第三,第四种方法,涉及到繁琐的递归和状态机相关原理,具体可以看:JSON.parse 三种实现方式

36.解析 URL Params 为对象

尽可能的全面正确的解析一个任意 url 的所有参数为 Object,注意边界条件的处理
要求如下:
• 1. 重复出现的 key 要组装成数组
• 2. 能被转成数字的就转成数字类型
• 3. 中⽂需解码
• 4. 未指定值的 key 约定为 true

let url = 'http://www.domain.com/?user=anonymous&amp;id=123&amp;id=456&amp;city=%E5%8C%97%E4%BA%AC&amp;enabled';
parseParam(url);
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/

具体实现代码思路如下,对url进行分割

function parseParam(url) {
    // 将 ? 后面的字符串取出来
    const paramsStr = url.split('?')[1];
    // 将字符串以 &amp; 分割后存到数组中
    const paramsArr = paramsStr.split('&amp;');
    // 将 params 存到对象中
    let paramsObj = {};
    for (let i = 0; i &lt; paramsArr.length; i++) {
        // 分割 key 和 value
        let [key, value] = paramsArr[i].split('=');
        // 处理没有值的参数,约定值为true
        if (!value) value = true;
        // 中文解码
        value = decodeURIComponent(value);
        // 转为数字类型,必须放中文解码后面
        if (/^\d+(\.\d+)?$/.test(value)) value = Number(value);
        // 处理重复出现的key,组装成数组
        if (paramsObj[key]) {
            // 主要是在这里做了一下处理,判断值是不是一个数组
            paramsObj[key] = Array.isArray(paramsObj[key]) ? [...paramsObj[key], value] : [paramsObj[key], value];
        } else {
            paramsObj[key] = value;
        }
    }
    return paramsObj;
}
let url = 'http://www.domain.com/?user=anonymous&amp;id=123&amp;id=456&amp;city=%E5%8C%97%E4%BA%AC&amp;enabled';
console.log(parseParam(url));

也可以使用正则表达式分割,这是网上找的大佬版本

function parseParam(url) {
     // 将 ? 后面的字符串取出来
    const paramsStr = /.+\?(.+)$/.exec(url)[1];
    // 将字符串以 &amp; 分割后存到数组中
    const paramsArr = paramsStr.split('&amp;');
    let paramsObj = {};
    // 将 params 存到对象中
    paramsArr.forEach(param =&gt; {
         // 处理有 value 的参数
        if (/=/.test(param)) {
            // 分割 key 和 value
            let [key, val] = param.split('=');
            // 递归调用解码
            val = decodeURIComponent(val);
            // 判断是否转为数字
            val = /^\d+$/.test(val) ? parseFloat(val) : val; 

            // 如果对象有 key,则添加一个值
            if (paramsObj.hasOwnProperty(key)) {
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else {
                // 如果对象没有这个 key,创建 key 并设置值
                paramsObj[key] = val;
            }
        } else {
            // 处理没有 value 的参数
            paramsObj[param] = true;
        }
    })
    return paramsObj;
}
let url = 'http://www.domain.com/?user=anonymous&amp;id=123&amp;id=456&amp;city=%E5%8C%97%E4%BA%AC&amp;enabled';
console.log(parseParam(url));

37.模板引擎实现

将对象data中的数据渲染至template模板中

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
    name: '姓名',
    age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined

自己跟着大佬手写的代码

function render(template, data) {
    // 模板字符串正则
    const reg = /\{\{(\w+)\}\}/;
    // 判断模板里是否有模板字符串
    if (reg.test(template)) {
        // 查找当前模板里第一个模板字符串的字段,对应正则表达式()中的内容
        const name = reg.exec(template)[1];
        // 将第一个模板字符串渲染
        template = template.replace(reg, data[name]);
        // 递归的渲染并返回渲染后的结构
        return render(template, data);
    }
    // 如果模板没有模板字符串直接返回
    return template;
}
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
    name: '姓名',
    age: 18
}
console.log(render(template, data)); // 我是姓名,年龄18,性别undefined

递归改为迭代

function render(template, data) {
    // 模板字符串正则
    const reg = /\{\{(\w+)\}\}/;
    // 判断模板里是否有模板字符串
    while (reg.test(template)) {
        // 查找当前模板里第一个模板字符串的字段,对应正则表达式()中的内容
        const name = reg.exec(template)[1];
        // 将第一个模板字符串渲染
        template = template.replace(reg, data[name]);
    }
    // 如果模板没有模板字符串直接返回
    return template;
}
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
    name: '姓名',
    age: 18
}
console.log(render(template, data)); // 我是姓名,年龄18,性别undefined

这只是自行车级别的模板引擎,想要火箭级别的请参考underscore 提供的模板引擎功能,冴羽大大提供了一步一步实现改模板引擎的手把手教程underscore 系列之实现一个模板引擎(上)underscore 系列之实现一个模板引擎(下)

38.驼峰命名-中划线转换

中划线转驼峰

// 把-后面的字母替换为大写字母
function fn(str) {
    return str.replace(/-\w/g, function (v) {
        // 首字母大写
        return v.slice(1).toUpperCase();
    })
}
let s1 = "get-element-by-id"; // 转化为 getElementById
console.log(fn(s1));

简化代码

// 把-后面的字母替换为大写字母
function fn(str) {
    return str.replace(/-\w/g, v =&gt; v[1].toUpperCase());
}
let s1 = "get-element-by-id"; // 转化为 getElementById
console.log(fn(s1));

或者

// 把-后面的字母替换为大写字母
function fn(str) {
    return str.replace(/-(\w)/g, (v1, v2) =&gt; v2.toUpperCase());
}
let s1 = "get-element-by-id"; // 转化为 getElementById
console.log(fn(s1));

驼峰转中划线

// 把大写字母替换为-和小写字母
function fn(str) {
    return str.replace(/[A-Z]/g, v =&gt; '-' + v.toLowerCase());
}
let s2 = "getElementById"; // 转化为 get-element-by-id
console.log(fn(s2));

拓展到4种模式

编程语言中常见的命名风格有如下四种:
1.全部首字母大写
2.第一个单词首字母小写,其余单词首字母大写
3.单词全部小写,由下划线连接
4.单词全部小写,由减号连接

请设计并实现一个caseTransform函数,使得一个字符串str可以被方便地转成四种形式,并且将四种形式通过空格拼接成一个字符串返回
为方便起见,这里假设输入字符串全部符合以上四种形式的英文字母组合

输入描述:

PascalCaseTest
输出描述:

PascalCaseTest pascalCaseTest pascal_case_test pascal-case-test
判断是哪种模式,识别之后进行拼接操作

function caseTransform(s) {
    let list = new Array(4);
    if (s.indexOf('_') != -1) {
        // 第3种情况
        // 自身
        list[2] = s;
        // 切割
        let arr = s.split('_');
        // 第4种模式
        list[3] = arr.join('-');
        // 第1种模式
        list[0] = arr.map((item) =&gt; item[0].toUpperCase() + item.slice(1)).join('');
        // 第2种模式
        list[1] = list[0][0].toLowerCase() + list[0].slice(1);
    } else if (s.indexOf('-') != -1) {
        // 第4种情况
        // 自身
        list[3] = s;
        // 切割
        let arr = s.split('-');
        // 第3种模式
        list[2] = arr.join('_');
        // 第1种模式
        list[0] = arr.map((item) =&gt; item[0].toUpperCase() + item.slice(1)).join('');
        // 第2种模式
        list[1] = list[0][0].toLowerCase() + list[0].slice(1);
    } else if (s[0] &gt;= 'A' &amp;&amp; s[0] &lt;= 'Z') {
        // 第1种情况
        list[0] = s;
        // 第2种模式
        list[1] = s[0].toLowerCase() + s.slice(1);
        // 第3种模式
        list[2] = list[1].replace(/[A-Z]/g, function(x) {
            return '_' + x[0].toLowerCase();
        });
        // 第4种模式
        list[3] = list[2].replace(/_/g, '-');
    } else {
        // 第2种模式
        list[1] = s;
        // 第1种模式
        list[0] = s[0].toUpperCase() + s.slice(1);
        // 第3种模式
        list[2] = s.replace(/[A-Z]/g, function(x) {
            return '_' + x[0].toLowerCase();
        });
        // 第4种模式
        list[3] = list[2].replace(/_/g, '-');
    }
    return(list);
}
let str = 'PascalCaseTest';
console.log(caseTransform(str).join(' '));

39.查找字符串中出现最多的字符和个数

排序+正则统计单个字符个数

1.使其按照⼀定的次序对数据进行排列

2.利用正则匹配数据

​ 反向引用

​ ()相关匹配会被存储到一个临时缓冲区,所捕获的每个子匹配都会按照正则模式中从左到右

​ 出现的顺序存储.缓冲区编号从1开始,最多99个捕获的子表达式,

​ 每个缓冲区都可用\n表示,其中 n 为一个标识特定缓冲区的一位或两位十进制数。如:

​ \1 指定第一个子匹配项,指定正则表达式的第二部分是对前面捕获的子匹配项的引用

​ 即第二个匹配项正好由括号表达式匹配.

3.利用replace的参数特性,得到最多字符及个数
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

语法:

​ stringObject.replace(regexp/substr,replacement)

参数:

​ regexp/substr: 规定子字符串或要替换的模式的 RegExp 对象

​ replacement: 规定了替换文本或生成替换文本的函数。

​ 可以是字符串,也可以是函数

​ 字符串: 每个匹配都由字符串替换

​ 函数:

​ 参数特性:

​ 第一个参数:匹配模式的字符串

​ 其他参数:模式中的子表达式匹配的字符串,可以有0或多个

​ 下一个参数:整数,声明匹配在stringObject 中出现的位置

​ 最后一个参数: stringObject本身

let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 使其按照一定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"

// 定义正则表达式
let re = /(\w)\1+/g;
str.replace(re, ($0, $1) =&gt; {
    if (num &lt; $0.length) {
        // num始终储存次数最大的那个
        num = $0.length;
        char = $1;
    } else if (num === $0.length){
        if (Array.isArray(char)) {
            char.push($1);
        } else {
            char = [char, $1];
        }
    }
})
console.log(`字符最多的是${char},出现了${num}次`);
// 字符最多的是c,出现了8次

哈希表统计单个字符个数

let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 哈希表
let obj = {};
// 使其按照一定的次序排列
for (let i = 0; i &lt; str.length; i++) {
    let char = str[i];
    if (obj[char]) {
        // 次数加1
        obj[char]++;
    } else {
        //若第一次出现,次数记为1
        obj[char] = 1;
    }
}
// 输出的是完整的对象,记录着每一个字符及其出现的次数
console.log(obj);
/*
    a: 3
    b: 5
    c: 8
*/

for (let key in obj) {
    if (num &lt; obj[key]) {
        num = obj[key];
        char = key;
    } else if (num === obj[key]){
        if (Array.isArray(char)) {
            char.push(key);
        } else {
            char = [char, key];
        }
    }
}
/*
    a: 3
    b: 5
    c: 8
*/
console.log(`字符最多的是${char},出现了${num}次`);

全部评论

(7) 回帖
加载中...
话题 回帖

推荐话题

相关热帖

热门推荐