js 笔记

43 阅读17分钟

1. 函数

1.1 内存管理

1. 为什么要内存管理?

1)在浏览器端,减少浏览器的负担,内存过大会让浏览器压力过大,导致浏览器卡顿;

2)在Node端,内存如果不够,服务就会中断。

2. V8引擎内存有多大

总大小:操作系统64位 1.4G,32位0.7G

默认情况下64位: 新生代(短时间存活的新变量叫新生代)内存32M,form区域和to区域各占比16MB。 老生代(生存时间比较长的变量)内存占比约1400M。

3. 新生代和老生代到底怎么回收?

  • 新生代简单的说就是复制,存放新产生的变量。首先存放在From空间,满足一定条件后,将还活着的变量复制到To空间中。然后清空From空间(全部清空节省时间)。往后,To变为From,之前清空的From变成新的To)。这就是牺牲空间换时间。牺牲一半空间,总有一半空间是空的。
  • 老生代:标记、删除、整理
    • 标记:先标记死掉的变量
    • 删除标记过的变量
    • 整理空间(同理于电脑磁盘碎片整理)让内存连续起来(数组在内存中存储必须是连续空间,所以必须要整理)

4.新生代和老生代如何转化

  • 刚开始定义的变量都是新生代,当变量在新生代经历过一次回收,就拥有资格晋升为老生代(但不会直接放入老生代)。
  • 直到新生代发现本次复制后,会占用超过百分之25的to空间,那么该变量就会晋升到老生代空间。

5.什么时候触发回收机制

    1. 执行完一次代码,回收一次
var a = 123; 
var b =1; 
cosole.log(a); 
setTimeout(()=> { 
    b++; 
    console.log(b); 
    // 回收一次 
}, 2000) 
// 回收一次
    1. 内存不够的时候
function testMemory() { 
    var memory = process.memoryUsage().heapUsed; 
    console.log(memory / 1024 / 1024 + "mb"); 
} 

var size = 30 * 1024 * 1024 
var arr1 = new Array(size); 
testMemory(); 
var arr2 = new Array(size); 
testMemory(); 
var arr3 = new Array(size); 
arr3 = undefined; 
testMemory(); 
var arr4 = new Array(size); 
arr4 = undefined; 
testMemory(); 
var arr5 = new Array(size); 
testMemory(); 
var arr6 = new Array(size); 
testMemory();
.... // 内存溢出 极限 8 个

那到底怎么判断一个变量可以回收呢?

  • 全局变量会直到程序执行完毕才会回收
  • 普通变量,当它们失去引用时就会回收

6.如何检测内存

  • 浏览器端 window.performance.memory
// window.performance.memory执行后得到: 
jsHeapSizeLimit: 4294705152 // 内存限制量 4294705152 Bit 
totalJSHeapSize: 120793358 // 总的堆内存 120793358 Bit 
usedJSHeapSize: 115916510 // 已使用的堆内存 115916510 Bit = 115916510/1024/1024 MB
  • Node端 process.memoryUsage()
// 执行 node,再执行 process.memoryUsage(),得到一个对象,如下 
{ 
    rss: 26271744, // node总占用内存(V8内存和C++内存的总量) 
    heapTotal: 6184960, // V8引擎的总内存 
    heapUsed: 4995720, // V8引擎的已使用的内存 
    external: 924009, // node专有(底层是c,额外申请到的c++内存 ) 
    arrayBuffers: 92365 // arrayBuffers 使用的内存 
}

7.优化的建议

  • 尽量不要定义全局变量,定义了及时手动释放(比如a=null或者a=undefined进行释放)
  • 注意闭包的使用

8.Node端的一些特殊点

  • Node可以手动触发垃圾回收 global.gc
  • Node端可以设置内存 node --max-old-space-size=1700 test.jsnode --max-new-space-size=1024test.js

9. 为什么V8要设计成1.4g

  • 1.4g 对于浏览器脚本来说够用
  • 垃圾回收的时候是阻塞式的,也就是进行垃圾回收的时候会中断代码的执行(内存设计得越大,中断代码执行的时间就会越长)

1.2 代码的性能指标

1. 代码健壮性(即代码抗击风险的能力)

  • 可读性
    • 代码结构
    • 命名规范
    • 注释
  • 可复用性
    • 重复的代码不写第二遍
    • 减少代码体积
  • 可扩展性

......

1.3 函数式编程

  • 面向过程(先做这个,再做那个,然后做什么)
  • 面向对象(把功能组织成对象,然后相关操作作为对象的方法)
  • 函数式编程(把功能分解为一系列独立的函数,通过函数间互相调用来完成功能)

1.4 组合执行多个函数-compose方法和pipe方法的区别

function multiplyTwo(num) {
    return num * 2;
}
function minusOne(num) {
    return num - 1;
}
function addTwo(num) {
    return num + 2;
}
function addThree(num) {
    return num + 3;
}
// 入门级组合执行方法
let result = multiplyTwo(10);
result = minusOne(result);
result = addTwo(result);
result = addThree(result);

// 使用 compose 方法组合执行
function compose() {
    const args = [].slice.apply(arguments);

    return function (num) {
        var _result = num;
        for (var i = args.length - 1; i >= 0; i--) {
            _result = args[i](_result);
        }
        return _result;
    }
    
    // 上面的for循环可以使用 reduceRight 方法实现一样的效果
    /* return args.reduceRight((res, cb) => cb(res), num);*/
}
console.log(compose(addThree, addTwo, minusOne, multiplyTwo)(10));


// 使用promise链式调用实现
Promise.resolve(10).then(multiplyTwo).then(minusOne).then(addTwo).then((res) => {
    console.log(res);
})

JavaScript中的composepipe都是用于组合多个函数的方法,它们的主要区别在于函数的执行顺序和参数传递方向。

compose方法将多个函数从右到左依次执行,并将每个函数的返回值作为下一个函数的输入参数传递。也就是说,compose方法的最右边的函数先执行,而最左边的函数最后执行。例如:

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

const add = x => x + 1;
const multiply = x => x * 2;
const square = x => x * x;

const composedFn = compose(add, multiply, square);
const result = composedFn(2); // ((2 * 2)^2) + 1 = 17
console.log(result); // output: 17

在上面的示例中,我们首先定义了三个函数addmultiplysquare,然后使用compose方法将它们组合起来形成一个新的函数composedFn。最后,我们调用composedFn方法并传入参数2,结果为17。

pipe方法与compose方法相似,但是它是从左到右依次执行多个函数,并将每个函数的返回值作为下一个函数的输入参数传递。也就是说,pipe方法的最左边的函数先执行,而最右边的函数最后执行。例如:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const add = x => x + 1;
const multiply = x => x * 2;
const square = x => x * x;

const pipedFn = pipe(square, multiply, add);
const result = pipedFn(2); // ((2^2) * 2) + 1 = 9
console.log(result); // output: 9

在上面的示例中,我们首先定义了三个函数addmultiplysquare,然后使用pipe方法将它们组合起来形成一个新的函数pipedFn。最后,我们调用pipedFn方法并传入参数2,结果为9。

总的来说,composepipe方法都可以用于将多个函数组合成一个新的函数,但是它们的执行顺序和参数传递方向不同。具体使用哪种方法取决于函数的具体需求。

1.5 函数柯里化

函数柯里化是一种函数式编程技术,它将一个接受多个参数的函数转换成一系列只接受单个参数的函数,这些函数的返回值是一个新的函数,用于接收下一个参数。最终返回的函数接受所有参数,并执行原始函数的功能。

函数柯里化的原理在于使用闭包将参数保存在内部函数中,返回一个新的函数,新函数接收新参数并结合之前保存的参数进行处理。

函数柯里化的主要应用场景是在函数式编程中,它可以让我们编写更具可组合性和可重用性的函数。同时,它也可以帮助我们简化代码,提高代码的可读性和可维护性。

函数的bind和柯里化有关系,bind方法可以绑定函数的上下文和参数,但是它并不能将一个多参数函数转换成柯里化函数,它只能将一个函数绑定上下文并返回一个新函数。

以下是一个函数柯里化的简单示例:

function add(a, b, c) {
  return a + b + c;
}

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6

在上面的例子中,我们定义了一个add函数,它接收三个参数并返回它们的和。然后我们定义了一个curry函数,它接收一个函数作为参数,并返回一个柯里化的版本。在curried函数中,我们使用闭包将参数保存在内部函数中,并根据参数的数量返回新的函数或直接执行原始函数。最后,我们使用curry函数将add函数转换为柯里化函数curriedAdd,并测试它在各种参数组合下的行为。

以下是一个简单的柯里化的例子,该函数接收两个参数,将它们相加并返回结果。使用柯里化后,可以先传入一个参数,之后再传入另一个参数,并返回结果。

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

const add5 = add(5); // 返回一个新函数
console.log(add5(3)); // 输出 8

在上面的例子中,add()函数返回了一个新函数,该函数可以接收一个参数y,并返回x + y的结果。调用add(5)会返回一个新的函数add5,该函数将5作为x的值保存在闭包中。之后调用add5(3)时,函数会取出之前保存的x的值5,加上新的参数3,并返回结果8

函数柯里化的优势在于它可以将一个需要多个参数的函数转化成多个需要单一参数的函数,这样可以更加方便地进行函数组合和复用。例如,我们可以使用函数柯里化来实现一个通用的计算器函数:

function calculate(operator) {
  return function (a) {
    return function (b) {
      switch (operator) {
        case '+':
          return a + b;
        case '-':
          return a - b;
        case '*':
          return a * b;
        case '/':
          return a / b;
      }
    };
  };
}

const add = calculate('+');
console.log(add(1)(2)); // 3

const subtract = calculate('-');
console.log(subtract(5)(3)); // 2

const multiply = calculate('*');
console.log(multiply(2)(4)); // 8

const divide = calculate('/');
console.log(divide(10)(2)); // 5

1.6 防抖和节流

防抖

有的操作是高频触发的,但是其实触发一次就好了,比如我们短时间内多次缩放页面,那么我们不应该每次缩放都去执行操作,应该只做一次就好。在比如监听输入框输入,不应该每次都去触发监听,应该是用户完成一段输入后,再进行触发。

总结:等用户高频事件完了,再进行事件操作

image.png

<!DOCTYPE html>
<html>

<head>
    <title>second</title>
</head>

<body>
    <input type="text" id="inputid" />
    <script>
        function debounce(fn, delay) {
            let timer = null;
            return function () {
                clearTimeout(timer);
                timer = setTimeout(() => {
                    fn.apply(this, arguments);
                }, delay)
            }
        }

        var inputdom = document.getElementById("inputid");
        inputdom.oninput = debounce(function (event) {
            console.log(event.target.value)
        }, 500)
    </script>
    </script>
</body>

</html>

节流

防抖存在一个问题,事件会一直到等到用户完成操作后一段事件再操作。如果一直操作,会一直不触发。如果这是一个按钮,点击就发送请求。如果一直点,那么请求就会一直不发出去。这里的正确思路应该是第一次点击就发送,然后上一个请求回来后,才能再发。

总结:某个操作希望上一次的完成后再进行下一次,或者说希望隔一定时间触发一次

image.png

<!DOCTYPE html>
<html>

<head>
    <title>second</title>
</head>

<body>

    <input id="inputid" />
    <button id="sendAxios">请求</button>
    <script>
        var inputdom = document.getElementById("inputid");
        function throttle(fn, delay) {
            let valid = true;
            return function () {
                if (valid) {
                    setTimeout(() => {
                        fn.apply(this, arguments);
                        valid = true;
                    }, delay)
                    valid = false;
                } else {
                    return false;
                }
            }
        }
        inputdom.oninput = throttle(function (event) {
            console.log(event.target.value)
        }, 200);
    </script>
</body>

</html>

总结

防抖和节流的相同点:
防抖和节流都是为了阻止操作高频触发,从而浪费性能。

防抖和节流的区别:
防抖是让你多次触发,只生效最后一次。适用于我们只需要一次触发生效的场景 节流是让你的操作,每隔一段时间才能触发一次。适用于我们多次触发要多次生效的场景。

1.7 underscore 和lodash

Underscore 和 Lodash 都是 JavaScript 的实用工具库,它们提供了一系列的方法和函数,用于简化 JavaScript 编程。它们的功能重叠很多,但 Lodash 更加强大,性能更好。

Lodash 是在 Underscore 的基础上发展而来的,提供了更多的方法和功能,并且相对于 Underscore 来说,Lodash 的性能更好,且支持的 JavaScript 环境更多。

因此,如果需要使用更多的方法和更好的性能,建议使用 Lodash。但是,如果您只需要一个轻量级的工具库来处理一些基本的数据操作,那么 Underscore 也是一个不错的选择。

2. 异步编程

2.1 简述事件循环闭环流程

当执行栈为空时,执行以下步骤

  1. 执行微任务队列 a. 选择微任务队列中最早的任务(任务x) b. 如果任务 x 为空(意味着微任务队列为空),跳转到步骤(g) c. 将 "当前正在运行的任务 " 设置为“任务 x " d. 运行 "任务 x " e. 将 "当前正在运行的任务 " 设置为空,删除 "任务x " f. 选择微任务队列中下一个最早的任务,跳转到步骤(b) g. 完成微任务队列;
  2. 选择宏任务队列中最早的任务(任务 A)
  3. 将"当前正在运行的任务" 设置为“任务 A"
  4. 运行"任务A"(表示运行回调函数)同步代码
  5. 跳到第 1 步。
  6. 将"当前正在运行的任务”设置为空,删除“任务 A" 结束本次Loop 循环
  7. 跳到第 2 步。

2.2 promise

标准promise的机制

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了。
  • 如果promise返回了成功的信息,那么你绑定在成功事件上的回调会得到这个消息。
  • 如果发生了错误,promise会收到一个带有错误信息的错误通知。

promise的缺点

  • 首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

2.3 Generator

进程、线程、协程

  • 进程
    进程是指在计算机中运行的一个程序。每个进程都有自己的内存空间和系统资源,包括打开的文件、网络连接和其他系统资源。每个进程都是独立的,彼此之间不会相互干扰。进程是操作系统进行资源分配和调度的基本单位,因此进程的切换会带来较大的系统开销。进程之间通常通过进程间通信(IPC)来进行数据传输和共享。
  • 线程
    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 协程
    协程是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

协程的Generator函数实现

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

2.4 async

  • async函数是什么?
    Generator 函数的语法糖:用更简练的言语表达较复杂的含义。在得到广泛接受的情况之下,可以提升交流的效率。 async 和 await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  • 返回值是Promise
    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部then命令的语法糖。

  • 更广的适用性
    async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

async function f() {
  return 'hello world';
}

f(); // 结果:Promise {<fulfilled>: 'hello world'}
f().then(v => console.log(v));  // 结果: "hello world"
async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)  // reject Error: 出错了

2.5 web Worker 多线程机制

Web Worker 是 HTML5 新增的特性之一,它允许在浏览器后台开启一个线程(或多个线程),并将这些线程用于执行 JavaScript 代码。这样就可以在主线程和工作线程之间分配任务,提高 JavaScript 执行效率,避免卡顿等问题。

Web Worker 有两种类型: Dedicated Worker 和 Shared Worker。

  • Dedicated Worker 是专用于某个页面的工作线程,只能被创建它的页面所使用,它的全局对象与主线程的全局对象是独立的,它们之间通过消息传递进行通信。
  • Shared Worker 是多个页面可以共享的工作线程,它可以被同一个域名下的多个页面所共享,每个页面与 Shared Worker 共享相同的全局对象,它们之间也通过消息传递进行通信。

Web Worker 的使用步骤如下:

  1. 创建 Worker 对象。在主线程中使用 new Worker() 方法创建 Dedicated Worker,使用 new SharedWorker() 方法创建 Shared Worker。
  2. 在 Worker 中编写 JavaScript 代码,通过 postMessage() 方法向主线程发送消息,通过 onmessage 事件监听主线程发送的消息。
  3. 在主线程中通过 worker.postMessage() 方法向 Worker 发送消息,通过 worker.onmessage 事件监听 Worker 发送的消息。

需要注意的是,由于 Web Worker 与主线程的全局对象是独立的,因此它们之间不能共享 DOM 对象、全局变量等资源,但是它们可以共享 ArrayBuffer、Blob、JSON 等数据。此外,Web Worker 运行在单独的线程中,无法访问主线程的 window、document 等对象,也无法操作 DOM 元素。

2.6 浏览器的 16ms 渲染帧

浏览器的 16ms 渲染帧是指在每个渲染周期内,浏览器需要在 16 毫秒内完成对网页进行重新渲染的过程。这个周期称为“帧”,通常称为“渲染帧”或“绘制帧”。

在浏览器中,当用户进行某些交互时,例如滚动页面、缩放页面或更改窗口大小时,浏览器需要重新计算页面布局,并进行重新渲染以显示新的页面内容。每个渲染帧的时间限制为 16 毫秒,这是为了确保浏览器可以在用户不感觉到延迟的情况下,尽快地响应用户的操作。

如果在渲染帧内无法完成重新渲染的过程,将会导致页面性能下降,出现卡顿、掉帧等问题,因此,为了确保网页的流畅性和用户体验,开发者需要优化页面的性能,以确保在每个渲染帧内能够尽可能地完成页面的渲染。

image.png

image.png

3. 设计模式

juejin.cn/post/713054…

4. 总结

  • 函数式编程
    优点:JS中函数式编程是一等公民,便于拆分组合,扩展性好,方便 tree-shaking
    缺点:管理难度大,复杂逻辑难以组织,模块难以划分
    一般是工具库和第三方库会使用函数式编程。

  • 面向对象编程
    优点:模块分明,逻辑清晰,方便组织庞大业务
    缺点:不好配合 tree-shaking,JS对于面向对象实现不完美