程序员成长-修炼中心 「作者:陈楚城」
导航
博客文章
  • Github (opens new window)
  • 掘金 (opens new window)
组件库 (opens new window)
关于我

chamberlain

前端持续学习者
导航
博客文章
  • Github (opens new window)
  • 掘金 (opens new window)
组件库 (opens new window)
关于我
  • 写在前面
  • vue3学习总结

  • 项目相关

  • 性能优化

  • 你不知道的css

  • 常见问题总结记录

  • 数据结构与算法

  • 设计模式

  • TS & JS进阶

    • typescript
    • recursive_optimization
    • functional_programming
    • 学习笔记
      • apply 、bind、call_
        • 手写call
        • 手写apply
        • 手写bind
      • 深拷贝 & 浅拷贝
        • 入门版本 - 浅拷贝
        • 基础版本
        • 考虑数组
        • 循环引用 - ( 导致栈内存溢出 )
        • 其他数据类型
        • 第三方深拷贝库
      • 防抖 | 节流
        • 什么叫函数防抖
      • 闭包及其应用
        • 词法作用域
        • 举个例子(闭包)
        • JS堆栈内存释放
  • Node

  • HTTP

  • Linux

  • 开发工具篇

  • 收藏夹

  • OS

  • Nginx

  • 项目工程化

  • 数据库

  • 计算机网络

  • 环境搭建、项目部署

  • 常用工具

  • 自动化

  • js相关

  • QA相关

  • 文章收藏

  • note
  • javascript
chamberlain
2022-04-15
目录

学习笔记

[TOC]

# apply 、bind、call_

前置知识:

function中的this一般有两种情况,

  • 第一种,是作为对象的内部的函数调用,则this指向这个对象
  • 第二种,作为全局的函数被调用,则this指向window

this指向:

  • 永远指向最后调用它的对象

# 手写call

    function show(...args){
        console.log(args)
        console.log(this.name);
    }
1
2
3
4

由于show是一个函数,在javascript中,所有的函数都是有Funcition这个构造函数进行是实例化的对象

接下来我们要调用一下show这个方法,但是由于内部内有定义name这个属性,故会报错

  • 怎么办 ? - 【 自己手写一个apply 修改show函数内部this 的指向 】

        Function.prototype.myCall = function(ctx,...args){
            // this 是Function生成的构造函数
             //   Function中的this ƒ show(...args){
             //   console.log(args)
            //    console.log(this.name);
           //       }
             // 步骤
            // 1  - 将方法挂载到我们传入的ctx
            // 2 -  将挂载以后的方法调用
            // 3 - 将我们添加的这个属性删除
            
            console.log('Function中的this',this);
            ctx.fn = this
            ctx.fn(...args)
        }
    	//  一般call函数需要接收2个参数,第一个参数是,所调用函数this的上下文,第2个参数就是所调用函数的形参
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

调用一下show函数

	show.myCall({name:'chamberlain'},'call1','call2','call3')

1
2

# 手写apply

    function show(...args){
        console.log(args)
        console.log(this.name);
    }

    Function.prototype.myApply = function(ctx,args = []){
        ctx.fn = this
        ctx.fn(...args)
        delete ctx.fn
    }

    show.myApply({name:'chucheng'},['jack','allen','martain'])

1
2
3
4
5
6
7
8
9
10
11
12
13

# 手写bind

    Function.prototype.myBind = function(ctx,...args1){
        console.log(...args1,'args1')

        return (args2)=>{
            ctx.fn = this
            ctx.fn(...args1.concat(args2))
            delete ctx.fn
        }
    }

    let bind =  show.myBind({name:'danina'},'jack','allen','martain')
    console.log(bind,'bind')

1
2
3
4
5
6
7
8
9
10
11
12
13

# 深拷贝 & 浅拷贝

浅拷贝:

image-20220425212351750

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

​ image-20220425212633941

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

那么如何进行浅拷贝和深拷贝呢?

# 入门版本 - 浅拷贝

JSON.parse(JSON.stringify());

1
2

这种写法非常简单,而且可以应对大部分的应用场景,但是它还是有很大缺陷的,比如拷贝其他引用类型、拷贝函数、循环引用等情况。

# 基础版本


function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

1
2
3
4
5
6
7
8
9

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。

如果是深拷贝的话,考虑到我们要拷贝的对象是不知道有多少层深度的,我们可以用递归来解决问题,稍微改写上面的代码:

  • 如果是原始类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上。

很容易理解,如果有更深层次的对象可以继续递归直到属性为原始类型,这样我们就完成了一个最简单的深拷贝:

function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};
1
2
3
4
5
6
7
8
9
10
11

这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是显然,他还有非常多的缺陷,比如,还没有考虑数组。


# 考虑数组

在上面的版本中,我们的初始化结果只考虑了普通的object,下面我们只需要把初始化代码稍微一变,就可以兼容数组了

module.exports = function clone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = clone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};
1
2
3
4
5
6
7
8
9
10
11

# 循环引用 - ( 导致栈内存溢出 )

说到循环引用,可能你会觉得有疑问、什么是循环引用?

循环引用就是对象的属性值应用了这个对象本身,就叫做循环引用,举个栗子:

let obj2 = {
  name: '循环引用测试'
}
obj2.cycle = obj2
let objClone2 = deepClone(obj2); // 报错栈溢出 Uncaught RangeError: Maximum call stack size exceeded
1
2
3
4
5
  • 栈溢出:

​ ECStack执行环境栈又叫调用栈,是JS引擎为了运行js代码在计算机中开辟的一块栈内存空间,用来管理函数调用关系。栈内存遵循先进后出的规律,并且这段栈内存空间是有限的。 当我们进行函数调用的时候,JS引擎会创建一个执行上下文并推进调用栈中供函数执行。当函数执行完毕,这个执行上下文会自动推出执行栈、释放空间。

​ 但是当我们执行上边的代码,因为深克隆会进行递归函数调用,在深层递归内的deepClone函数没执行完毕前,先推进执行栈的函数就不能结束、不能出栈。 又因为我们现在obj2循环引用了自己,我们的deepClone就要不停的循环递归。这就导致了一直往栈中塞数据、但从不释放。栈空间就像一个水杯一样,被我们一直“加水”直至“溢出”并报错。

解决方案:

​ 解决循环引用导致的栈溢出问题,就需要我们判断要拷贝的对象,是不是已经拷贝过,而不要循环拷贝。

​ 我们可以利用缓存的思想,额外创建一个哈希映射表(字典集合,其实就是一个缓存对象),来存储当前对象和拷贝对象的对应关系。 ​ 哈希映射表需要key: value这种键值对结构,并且要满足key可能是引用类型的要求(一般情况下key是字符串类型)。 ​ 后续使用时,每拷贝一个引用值,就记录(缓存)到集合中。下次拷贝时,先检查是否在缓存中: ​ | - 若有,直接取缓存; ​ | - 若无,实行拷贝并缓存。

解决方向:

​ -- 因为我们实际是要创建一个缓存对象,但我们的key又要可以是引用类型。 ​ -- JS提供的原生对象虽然就是键值对的集合,但是传统上对象只能用字符串当作键。不太满足我们的条件。 ​ -- 为此,我们的解决方向上,可以考虑用ES6新增的数据结构:Map、WeakMap

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 WeakMap结构与Map结构类似,也是用于生成键值对的集合。 来源:https://es6.ruanyifeng.com/#docs/set-map#Map

但是WeakMap比Map有两个不同:

1、【特殊点】WeakMap只接受引用类型(对象)作为键名; 2、【优点】WeakMap的键名所指向的对象都是弱引用,不计入垃圾回收机制,不用考虑内存泄漏。 当引用对象被清除,其所对应的WeakMap记录就会自动被移除。(具体请看ES6相关解释,这里不展开。)

为什么要这样做呢?,先来看看WeakMap的作用:

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

举个例子:

如果我们使用Map的话,那么对象间是存在强引用关系的:

let obj = { name : 'ConardLi'}
const target = new Map();
target.set(obj,'code秘密花园');
obj = null;
1
2
3
4

虽然我们手动将obj,进行释放,然是target依然对obj存在强引用关系,所以这部分内存依然无法被释放。

再来看WeakMap:

let obj = { name : 'ConardLi'}
const target = new WeakMap();
target.set(obj,'code秘密花园');
obj = null;
1
2
3
4

如果是WeakMap的话,target和obj存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

设想一下,如果我们要拷贝的对象非常庞大时,使用Map会对内存造成非常大的额外消耗,而且我们需要手动清除Map的属性才能释放这块内存,而WeakMap会帮我们巧妙化解这个问题。

简而言之就是:Map是高级版的Object,WeakMap是高级版的Map

Map \ weakMap:

综合所有考量,我们启用最优解——WeakMap来实现这个缓存字典。

伪代码思路: 1、检查 weakMap 中有无克隆过的对象。 2、有,直接返回 3、没有,将当前对象作为key,克隆对象作为value进行存储 4、继续克隆

       /*
        * @param {object} obj 
        * @param {*} hashMap WeakMap数据,用于缓存克隆过的对象
        * @returns obj_new / 克隆的obj
        */

    function deepClone(obj,hashMap = new WeakMap()){
        if (obj === null) return
        if (obj instanceof Date) return new Date(obj);
        if (obj instanceof RegExp) return new RegExp(obj);
        if (typeof obj!=="object" ) return obj
        // 查缓存字典中是否已有需要克隆的对象,有的话直接返回同一个对象
        //(同一个引用,不用递归无限创建进而导致栈溢出了)
        if (hashMap.has(obj)) return hashMap.get(obj);
        hashMap.set(origin, result); // 哈希表缓存新值
        let obj_new = Array.isArray(obj)?{}:[]
        // 遍历对象中的元素
        for(let k in obj){
            if(typeof obj[k]=='object' || typeof obj[k]=='array'){
                obj_new[k]  = deepClone(obj[k])
            }else {
                obj_new[k] = obj[k]
            }
        }
        return obj_new
    }

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

# 其他数据类型

在上面的代码中,我们其实只考虑了普通的object和array两种数据类型,实际上所有的引用类型远远不止这两个,还有很多,下面我们先尝试获取对象准确的类型

# 合理的判断引用类型

首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型:

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
1
2
3
4
if (!isObject(target)) {
    return target;
}
// ...
1
2
3
4

我们可以使用toString来获取准确的引用类型:

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

function getType(target) {
    return Object.prototype.toString.call(target);
}
1
2
3

image-20220425232906115

下面我们抽离出一些常用的数据类型以便后面使用:

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
1
2
3
4
5
6
7
8
9
10
11
12

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型
  • 不可以继续遍历的类型

# 第三方深拷贝库

该函数库也有提供_.cloneDeep用来做 Deep Copy(lodash是一个不错的第三方开源库,有好多不错的函数,也可以看具体的实现源码)

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
1
2
3
4
5
6
7
8

# 防抖 | 节流

# 什么叫函数防抖

概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

举个栗子,坐电梯的时候,如果电梯检测到有人进来(触发事件),就会多等待 10 秒,此时如果又有人进来(10秒之内重复触发事件),那么电梯就会再多等待 10 秒。在上述例子中,电梯在检测到有人进入 10 秒钟之后,才会关闭电梯门开始运行,因此,“函数防抖”的关键在于,在 一个事件 发生 一定时间 之后,才执行 特定动作。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        手机:<input type="text"> 
        <input type="submit" class="sumbit" id="input">
    </div>
</body>
<script>
    let btn  = document.querySelector('#input')
    btn.addEventListener('click',debounce(sumbit),false)

    function sumbit(e) {
        console.log(this,'指向btn这个dom元素');
    }
    // 防抖函数
    function debounce(fn,delayTime) { 
        /* 这里的的函数因为是定义在全局下的,通过function声明,所以这里的this指向的是windows */
        var timer = null
        return function () {  
             /* 
                这里的function因为作为返回值,所以其函数作用域还是属于按钮的点击事件那里
                故这里的this指向的是按钮本身,也就是这个btn对应的dom元素     */
            if(timer){
                clearTimeout(timer)
            }
            timer =  setTimeout(() => {
                /* 
                  由于最后在sumbit中的this需要指向btn
                  所以通过debounce调用这个sumbit后,里面的this需要保持不变,所以需要通过apply指向箭头函数()=>{}上一级的this变量   */
              
                fn.apply(this)

            }, delayTime);
        }
     }
</script>
</html>
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
32
33
34
35
36
37
38
39
40
41
42
43
44

拓展:对于函数内部的this指向问题,函数最后是谁调用的,则内部的this就指向谁

# 闭包及其应用

什么是闭包 (opens new window)?我们来看一下官方对于闭包的解释吧:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来

# 词法作用域

function init() {
  var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  function displayName() { // displayName() 是内部函数,一个闭包
      alert(name); // 使用了父函数中声明的变量
  }
  displayName();
}
init();
1
2
3
4
5
6
7
8

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。

# 举个例子(闭包)

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();
1
2
3
4
5
6
7
8
9
10

运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。

第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 makeFunc() 执行完毕,你可能会认为 name 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中。

# JS堆栈内存释放

  • 堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址

  • 栈内存:提供代码执行的环境和存储基本类型值。

  • 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。

但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放

https://juejin.cn/post/6937469222251560990

更新时间: 4/26/2022, 6:52:05 PM
functional_programming
node

← functional_programming node→

最近更新
01
02
网站
06-10
03
nav
06-09
更多文章>
Theme by Vdoing | Copyright © 2019-2022 chamberlain | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式