Xin's blog Xin's blog
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
    • Vue
    • 组件与插件
    • CSS扩展语言
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《TypeScript 从零实现 axios》
    • 《Git》学习笔记
    • TypeScript笔记
    • JS设计模式总结笔记
  • 前端框架面试题汇总
  • 基本面试题
  • 进阶面试题
  • 其它
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 前后端联调
  • mock.js
  • 奇技淫巧
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

Xin

英雄可不能临阵脱逃啊~
首页
  • 前端文章

    • HTML
    • CSS
    • JavaScript
    • Vue
    • 组件与插件
    • CSS扩展语言
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《TypeScript 从零实现 axios》
    • 《Git》学习笔记
    • TypeScript笔记
    • JS设计模式总结笔记
  • 前端框架面试题汇总
  • 基本面试题
  • 进阶面试题
  • 其它
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 前后端联调
  • mock.js
  • 奇技淫巧
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • HTML

  • CSS

  • JavaScript

    • 知识点

    • 方法

      • js能力提升1
        • 数组去重
        • 数组扁平化
        • 深拷贝
        • 图片懒加载
        • 函数防抖
        • 函数节流
          • 实现原理
          • 避免this的指向丢失
          • 总结
        • 函数柯里化
          • 什么是柯里化
          • 柯里化的实现
        • 实现数组原型方法
          • forEach
          • map
          • filter
          • some
          • reduce
        • 实现函数原型方法
          • call
          • apply
          • bind
      • js能力提升2
      • JS随机打乱数组
      • 判断是否为移动端浏览器
      • 将一维数组按指定长度转为二维数组
      • 防抖与节流函数
      • JS获取和修改url参数
      • js常用方法
      • js数据类型及类型判断
      • 用最简洁的代码去实现indexOf
      • 清空数组的方法
      • filter()方法过滤时如何保留假值?
      • 监听img元素是否加载完成
      • 树形结构转一维数组
      • 过滤数两个数组中重复的元素
      • 因为数据类型导致的排序错乱问题
  • Vue

  • 组件与插件

  • css扩展语言

  • 学习笔记

  • 前端
  • JavaScript
  • 方法
ctrlwin
2021-04-07

js能力提升1

原文:死磕 36 个 JS 手写题(搞懂后,提升真的大) (opens new window)

# 数组去重

ES5实现:

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

// reduce()实现
function unique(arr){
    if(!arr.length) return
    return arr.reduce((pre,cur)=>
        pre.includes(cur)?pre:pre.concat(cur),[]
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

ES6实现:

var unique = arr => [...new Set(arr)]
1

Set是ES6新提供的数据结构,类似于数组,但是本身没有重复值。

# 数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]
1

现在就是要实现 flat 这种效果。

ES5 实现:递归。

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result;
}

// reduce()实现
function flatten(arr){
    if(!arr.length) return
    return arr.reduce((pre,cur)=>
        Array.isArray(cur)?[...pre,...flatten(cur)]:[...pre,cur],[]
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

ES6实现:

function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
1
2
3
4
5
6

es6的扩展运算符能将二维数组变为一维。

根据这个结果我们可以做一个遍历,若arr中含有数组则使用一次扩展运算符,直至没有为止。

# 深拷贝

简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。

function deepClone(obj) {
    // 不是object类型不拷贝
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 判断子元素类型是否是object,是则将子元素递归,否则直接赋值
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}
1
2
3
4
5
6
7
8
9
10
11
12

# 图片懒加载

与普通的图片懒加载不同,如下这个多做了 2 个精心处理:

  • 图片全部加载完成后移除事件监听;
  • 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

const imgLazyLoad = function() {
    let count = 0
    // 修正错误,需要加上自执行
-   return function() {
+   return (function() {
        let deleteIndexList = []
        imgList.forEach((img, index) => {
            let rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
                img.src = img.dataset.src
                deleteIndexList.push(index)
                count++
                if (count === length) {
                    document.removeEventListener('scroll', imgLazyLoad)
                }
            }
        })
        imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
-   }
+   })()
}

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

参考:图片懒加载 (opens new window)

# 函数防抖

触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。

简单版:函数内部支持使用 this 和 event 对象;

function debounce(func, wait) {
    var timeout;
    return function () {
        // 修正this指向window的问题
        var context = this;
        // 解决函数的事件对象 event 变成了 undeined的问题
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function () {
            // 传入参数
            func.apply(context, args)
        }, wait)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用:

var node = document.getElementById('layout')
function getUserAction(e) {
    console.log(this, e)  // 分别打印:node 这个节点 和 MouseEvent
    node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)

1
2
3
4
5
6
7

# 函数节流

当持续触发事件时,保证一定时间段内只调用一次事件处理函数。也就是一个函数执行一次后,只有大于设定的执行周期后才会执行第二次。

记忆法:联系到水流的流量,我想让你1s只流出多少水你就只能流多少水,多的水流只能等到下个周期才能流出。

应用场景:如用户不断滑动滚轮,规定1s只能真正下滑一次,你滑再多也没用,只能等到下个周期你再滑才有用。

# 实现原理

A:用函数的闭包来锁住上一执行的时间,在用这一次执行的时间相比,大于设定的间隔时间则执行

B:也可以直接把lasTime放到全局去,不用闭包但这样就不好在事件监听的时候传递参数delay只能写死

# 避免this的指向丢失

1.throttle函数在全局执行,内部this通常是指向window的,然后返回一个匿名函数。

2.返回的匿名函数绑定了事件,this指向监听的元素(document)

3.fn如果直接用fn()这样的函数调用模式,this是绑定到全局的(非严格模式下),这里需要特殊处理

4.这里用apply修正this指向,使fn内部的this重新指向document

<script type="text/javascript">
    function throttle(fn, delay) {
        console.log(this) //window
        // 记录上一次函数触发的时间
        var lastTime = 0;
        return function () {
            var context = this
            var args = arguments
            // 记录当前函数触发的时间
            var nowTime = +new Date();
            if (nowTime - lastTime > delay) {
                fn.apply(context, args) // 修正this指向问题
                console.log(this) //document
                // 同步时间
                lastTime = nowTime;
            }
        }
    }
    document.onscroll = throttle(function () {
        /*console.log(this)//window*/
        console.log(this) //document
        console.log('scroll事件被触发了' + Date.now())
    }, 1000)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 总结

函数防抖:将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数。原理是通过判断nowTime与lastTime的差值是否大于wait的时间,大于才会触发函数。

二者区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

# 函数柯里化

# 什么是柯里化

柯里化,是函数式编程的一个重要概念。它既能减少代码冗余,也能增加可读性。另外,附带着还能用来装逼。

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

柯里化的定义,理解起来有点费劲。为了更好地理解,先看下面这个例子:

function add (a, b, c) {
    console.log(a + b + c);
}
add(1, 2, 3); // 6
1
2
3
4

add函数的柯里化函数_add则可以如下:

function _add(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}
1
2
3
4
5
6
7

下面的运算方式是等价的。

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

# 柯里化的实现

// 简单实现,参数只能从右到左传递
function createCurry(func, args) {
	// 函数参数的长度
    var arity = func.length;
    // 用来存放上一层收集的参数(第一次调用没有参数所以赋值为空数组)
    var args = args || [];

    return function() {
        // 收集参数
        var _args = [].slice.call(arguments);
        // 添加上一层传递的参数
        [].push.apply(_args, args);

        // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if (_args.length < arity) {
            return createCurry.call(this, func, _args);
        }

        // 参数收集完毕,则执行func
        return func.apply(this, _args);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个createCurry函数的封装借助闭包与递归,实现了一个参数收集,并在收集完毕之后执行所有参数的一个过程。

# 实现数组原型方法

# forEach

Array.prototype.forEach2 = 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)  // this 就是当前的数组
    const len = O.length >>> 0  // 后面有解释
    let k = 0
    while (k < len) {
        if (k in O) {
            callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

参考:forEach#polyfill (opens new window)

Object(this)的作用:如果使用基本数据类型作为this的指向,在非严格模式下都会被包装成一个对象,但是在严格模式下不会包装。为了不影响后续的使用,所以使用Object(this)将this的指向统一包装成对象。

O.length >>> 0是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型。感兴趣可以阅读 something >>> 0是什么意思? (opens new window)。

# map

基于 forEach 的实现能够很容易写出 map 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = 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
+   let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
+           res[k] = callback.call(thisArg, O[k], k, O);
        }
        k++;
    }
+   return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# filter

同样,基于 forEach 的实现能够很容易写出 filter 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = 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
+   let k = 0, res = []
    while (k < len) {
        if (k in O) {
-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               res.push(O[k])                
+           }
        }
        k++;
    }
+   return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# some

同样,基于 forEach 的实现能够很容易写出 some 的实现:

- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = 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) {
-           callback.call(thisArg, O[k], k, O);
+           if (callback.call(thisArg, O[k], k, O)) {
+               return true
+           }
        }
        k++;
    }
+   return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# reduce

Array.prototype.reduce2 = 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, acc
    
    if (arguments.length > 1) {
        acc = initialValue
    } else {
        // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
        while (k < len && !(k in O)) {
            k++
        }
        if (k > len) {
            throw new TypeError( 'Reduce of empty array with no initial value' );
        }
        acc = O[k++]
    }
    while (k < len) {
        if (k in O) {
            acc = callback(acc, O[k], k, O)
        }
        k++
    }
    return acc
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 实现函数原型方法

# call

使用一个指定的 this 值和一个或多个参数来调用一个函数。

实现要点:

  • this 可能传入 null;
  • 传入不固定个数的参数;
  • 函数可能有返回值;
Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# apply

apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。

实现要点:

  • this 可能传入 null;
  • 传入一个数组;
  • 函数可能有返回值;
Function.prototype.apply2 = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# bind

bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

实现要点:

  • bind() 除了 this 外,还可传入多个参数;
  • bing 创建的新函数可能传入多个参数;
  • 新函数可能被当做构造函数调用;
  • 函数可能有返回值;
Function.prototype.bindFn = function (thisArg){
    if(typeof this !== 'function'){
        throw new TypeError(this + 'must be a function');
    }
    // 存储函数本身
    var self = this;
    // 截取除thisArg外的其它数据并放到数组中
    var args = [].slice.call(arguments, 1);
    // 定义bind返回的新函数
    var bound = function(){
        // 将新函数的参数转成数组
        var boundArgs = [].slice.call(arguments);
        // apply修改this指向,把两个函数的参数合并传给self函数,并执行self函数,返回执行结果
        return self.apply(thisArg, args.concat(boundArgs));
    }
    return bound;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在GitHub上编辑 (opens new window)
#js
上次更新: 2/23/2022, 5:36:03 PM

← clipboard 剪切板属性 js能力提升2→

最近更新
01
createElement函数创建虚拟DOM
05-26
02
clipboard 剪切板属性
05-26
03
vue的权限管理
05-16
更多文章>
Theme by Vdoing | Copyright © 2021-2022 Xin | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×