likes
comments
collection
share

我所知道的JavaScript——实用的数组小技巧分享

作者站长头像
站长
· 阅读数 27

前言

最近一直在刷算法题,从5月份开始,到现在已经刷了200多道了✊。

我所知道的JavaScript——实用的数组小技巧分享 从下图可以看出,我遇到了很多类型的题目,比如数组、深度优先搜索、回溯(剪枝)等等。

我所知道的JavaScript——实用的数组小技巧分享

在使用JavaScript写这些题目的时候,我被动地了解了很多有关数组的小技巧。

🔈为什么是被动地了解?

因为题目写不出来的时候,看别的同学用JavaScript写的高质量题解,从他们的题解里看到了一些自己以前完全没注意过也没用过的写法,不禁令人感慨,题目不会写就算了,连题解都看不懂😭。

而且还发现了闭包的另一种用途,可以用来处理记忆化搜索。

在接触这些“新”知识并且实际应用了很多天之后,我想,这些技巧应该不仅仅只局限于在算法题的代码中使用,也可以在我们的业务开发代码中应用。

那么就让我们一起来看看,都有哪些技巧(以及都踩了哪些坑🕳️)吧~

我所知道的JavaScript——实用的数组小技巧分享

数组

在这一章节,我会通过设问的方式,结合实际的代码例子,将一些数组使用上的小技巧逐一展现。并且会在章节末尾附上一些与之相关的力扣算法题的题号,大家感兴趣的话,也可以亲自动手尝试~

创建数组

使用数组字面量

写到这里的时候,我觉得可以回顾一下知识,于是我们先聊一聊什么是字面量

在编程语言中,字面量(Literal)是一种表示源代码中固定值的表示法。在 JavaScript 中,你可以使用各种字面量。这些字面量是脚本中按字面意思给出的固定的值,而不是变量。

那么在JavaScript中,字面量都有哪些呢?

以下是一些常见数据类型的字面量示例:

  1. 字符串字面量

    let stringLiteral = "Hello, Juejin!";
    
  2. 数字字面量

    let numericLiteral = 6;
    let floatLiteral = 3.1415926;
    
  3. 布尔字面量

    let booleanLiteral = true; // 或 false
    
  4. 数组字面量

    let arrayLiteral = [1, 2, 3, 4];
    
  5. 对象字面量

    let objectLiteral = { name: "Look out", age: 24 };
    
  6. 函数字面量

    let functionLiteral = function(name) { console.log("Hello, " + name); };
    
  7. Symbol字面量(ES6+):

    let symbolLiteral = Symbol("uniqueKey");
    
  8. 模板字面量(ES6+):

    let templateLiteral = `The sum of 8 + 23 is ${8 + 23}`;
    

说到这里,我们是不是会发现,其实平常和同事交流,我们并不会把“字面量”挂在嘴边,我们往往说的是“”:

  • “这里传给这个组件的API应该是true。”
  • “这里这个通用函数返回的是客户合同金额。”

或者有些时候连“”都省略了,直接说的类型

  • “后端老哥,这里请你返回一个对象给我。”
  • “打断点后,可以看到这个接口的入参是个undefined

假如咱们把上面的日常聊天,加上“字面量”的话......

“请返回一个对象字面量给我。”

“这个函数返回的是一个数字字面量,表示客户合同金额。”

“这个组件的API接收的是布尔字面量,你可以传trueorfalse

我所知道的JavaScript——实用的数组小技巧分享

言归正传,我们继续聊回数组。

  • let array1 = []:这种方式代码是最简单的,将数组字面量赋值给array1,创建了一个空数组。

使用Array()构造函数

  • let array1 = new Array()
  • let array2 = Array()

👆上方这两种写法,都可以成功创建空数组。

为什么省略new和加上new最终的结果是一样的?

我们可以去查看一下ECMAScript® 2025 Language Specification (tc39.es)

我所知道的JavaScript——实用的数组小技巧分享

👆注意看上图中红色箭头指着的那一行

翻译一下📚:当作为构造函数调用时,会创建并初始化一个新的数组。 当作为函数而不是构造函数调用时,同样会创建并初始化一个新的数组。因此,函数调用 Array(…) 与使用相同参数的对象创建表达式 new Array(…) 是等价的。

我们通过阅读ECMAScript规范,再次确认了2种方式的等价性,因此我们只要记住这个特性就可以了。并且最好还是遵循代码编写的规范,每次用时,都把new操作符加上。

不过如果你也抱有下方这样的疑问👇,并期望尝试去追根溯源

不行,我就是好奇,为啥它们能达到一样的效果,Array()构造函数的内部实现是不是有一段特殊的逻辑?

巧了,那么你可以阅读本文的最后章节——【拓展】章节,一起踏上探索的旅程。

初始化的扩展

在上一小节,我们提到的创建数组的方式都是空数组,如果我期望创建一个定长的数组呢,比如一个长度100的数组?

  • const array3 = new Array(100):将期望的长度数字传入即可。不过此数字有限制(介于 0 到 232 - 1(含)之间的整数)

当我们做有一些算法题的时候,我们期望不仅仅是初始化一个固定长度的数组,还希望此数组的数组成员默认都是0,此时该怎么做(比如:242.有效的字母异位词)?

  • const array3 = new Array(26).fill(0):通过调用实例方法Array.prototype.fill()来实现用一个固定值填充一个数组从起始索引到结束索引内的全部元素。

使用Array.from()方法

Array.from() 静态方法从可迭代类数组对象创建一个新的浅拷贝的数组实例。

聊到它,就不得不提经典的JavaScript面试题:数组去重。

数组去重的方式有很多种,如果是结合ES6去实现的话,我们可以这么去写:

const repeateArray = [1, 1, 1, 1, 1, 2, 3, 4, 5]
const uniqueArray = Array.from(new Set(repeateArray))
// uniqueArray: [1, 2, 3, 4, 5]

当然,这种去重的方式只适用于数组成员是数字、字符串、布尔值等原始值的数组。

上面这句话的意思并不是说你不能将对象传递给SetSet 对象允许你存储任何类型(无论是原始值还是对象引用)的唯一值。

但是,如果你传入了两个引用地址不同但是内容相同的对象,你期望Set能帮你去掉重复内容的对象时,Set并不能做到。

因为对于Set来说,它存储的是引用地址,而这俩对象的引用地址不同,则表示它们都是集合中唯一的元素,因此并不会有任意一方会被Set去掉。

举个例子:

const repeateObjectArray = [1, {a:1}, {a:1}, {b:2}, 1, 2, 3, 4, 5]
const uniqueArray = Array.from(new Set(repeateObjectArray))
// uniqueArray: [1, {a:1}, {a:1}, {b:2}, 2, 3, 4, 5]


初始化的扩展

在这一小节,我们也要讲一讲使用Array.from()方法如何去便捷地完成一些数组的初始化。

如何初始化一个7 * 8且全部元素初始值为0的二维数组?

  • 方法一,循环

    const matrix = new Array(7);
    for(let i = 0; i < 7; i++){
        matrix[i] = new Array(8).fill(0)        
    }
    

    我所知道的JavaScript——实用的数组小技巧分享

  • 方法二,1行代码

    const matrix2 = Array.from({length:7},()=>new Array(8).fill(0)) 
    

    我所知道的JavaScript——实用的数组小技巧分享

可以看到,当我们创建多维数组时,使用Array.from()是更优雅的方式。

和初始化二维数组相关的算法题:

如何将一个字符串变成数组?

通常,我们采用String.prototype.split()去实现,比如:

"Juejin".split('')
// ['J', 'u', 'e', 'j', 'i', 'n']

但其实我们也可以使用Array.from()去实现,比如:

Array.from("Juejin")
// ['J', 'u', 'e', 'j', 'i', 'n']

如何快速生成序列化数组?

当我在写力扣的经典面试150题中的哈希表部分时,我遇到了这样一道题目290.单词规律,在解这道题的时候,我需要一个包含26位小写字母且符合字母表排列顺序的数组,便于我后续的映射工作。如何能够快速生成这样的一个数组呢?

按照顺序自己手动敲一下自然是没问题的,但是这费时间呀,我得先敲一个单引号,然后又敲一个字母,之后再敲一个单引号,然后最后再敲一个逗号。敲好一份后,赋值25遍,再一个个修改字母的内容,最终得到这样一份数据:

const alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

这听着、看着都麻烦呀。

我所知道的JavaScript——实用的数组小技巧分享

那么这种时候,我们也可以使用Array.from(),让它帮我们快速生成序列化数组。


 const alphabet =Array.from({ length:26 }, (_, i) => 
 String.fromCharCode('a'.charCodeAt() + i));

// ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

如果要换成大写字母的字母表,也很方便,修改一个字符即可

 const capAlphabet =Array.from({ length:26 }, (_, i) => 
 String.fromCharCode('A'.charCodeAt() + i));
 // ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

上述方式都是我在写题的过程中真实遇到的问题以及解决这些问题的小技巧。如果你期望更加全面地了解Array.from(),可以查看MDN上的相关内容,我就不再赘述了,因为MDN上真的写得好呢。

Array.from() - JavaScript | MDN (mozilla.org)

数组操作

两两交换

在我写过的算法题中,往往会有数组元素之间互相交换的情况出现,遇到这种情况,通常我们会这么写:

const nums = Array.from({length:20},(_,i)=>i+1);
//  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
for(let i = 0; i < 19;i+=2){
    let temp = nums[i];
    nums[i] = nums[i+1];
    nums[i+1] = temp;
}
// 两两一组,前后互换
//  [2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 18, 17, 20, 19]

👆上面这样的写法,要写3行,是不是也很麻烦。

(我是真的一行代码都不想多写🤯)

此时我们可以使用ES6引入的新特性,解构赋值,用1行代码搞定,如下:

const nums = Array.from({length:20},(_,i)=>i+1);
//  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
for(let i = 0; i < 19;i+=2){
    [nums[i],nums[i+1]] = [nums[i+1],nums[i]];
}
// 两两一组,前后互换
//  [2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 18, 17, 20, 19]

CodeReview

之前我对团队里的其他同学进行代码评审的时候,看到有位同学在一个条件语句是这么写的,我用代码模拟复现一下:

场景:只有所有的开关都处于闭合状态(即值为true),才能够进行接口的调用,否则,弹窗提示。

const switchStatus  = [ true, true, true, false, true]
if(switchStatus.filter(item=>item === false).length === 0){
    // do something
    console.log("发起接口调用")
}

可以看到,这种写法,在if语句里面是非常长的一段,而且从代码可读性上来说,也不够好。这种时候怎么去修改这里的代码呢?

很显然,这位同学可能对于Array的实例方法了解的不多,或者说忘记了。Array.prototype.filter()虽然能够解决,但是并不能很优雅地解决。

这里应该使用Array.prototype.every(),让我们来修改一下代码:

const switchStatus  = [ true, true, true, false, true]
if(switchStatus.every(item=>item)){
    // do something
    console.log("发起接口调用")
}

换成Array.prototype.every()后,我们通过阅读代码本身,而无需任何多余的注释,也能快速明白这里的条件语句是表达“只有每个开关都闭合”的意思了。

换成Array.prototype.some()也可以:

const switchStatus  = [ true, true, true, false, true]
if(!switchStatus.some(item=>item === false)){
    // do something
    console.log("发起接口调用")
}

只要开关中存在1个处于断开(即没有闭合,false)状态,则不进入条件语句的逻辑。

这两种方式都比使用Array.prototype.filter()要更合适。

闭包

为什么刷算法题,还能够用到JavaScript的闭包?

当我们写到部分使用深度优先搜索的算法题时,它给的数据量可能会很大,如果我们不采用记忆化搜索的方式去优化我们的代码,则我们程序的运行时间会大概率超过题目限制的运行时间,从而导致我们答题失败。

这是很可惜的一件事情,因为我们好不容易才想到了题目的解法,明明可以AC的,却倒在了时间复杂度上。

于是为了避免这种可惜的情况,我们可以使用闭包来完成记忆化搜索,优化我们的代码。

所谓的记忆化搜索也就是缓存,缓存某个参数下函数的执行结果,在后续遇到相同的参数时,不再执行函数,而是从缓存直接返回结果,极大地提升了程序的运行速度。

我来用一道题目作为例子:3040. 相同分数的最大操作数目 II - 力扣(LeetCode)

这里我仅仅只讲如何用闭包实现函数的缓存装饰器,如果大家对这道算法题感兴趣,可以查看我在【拓展章节附上的题解(题解是我本人写的,挺适合新手阅读。)

我们直接来看代码:

// 使用闭包和高阶函数来实现缓存装饰器
function cache(func) {
  const memo = new Map();

  return function(...args) {
    const key = JSON.stringify(args);
    if (!memo.has(key)) {
      memo.set(key, func.apply(this, args));
    }
    return memo.get(key);
  };
}
  • 当 cache 函数被调用时,它创建了一个新的 Map 对象 memo,用于存储函数调用的结果。
  • cache 函数返回一个新的匿名函数,这个匿名函数在调用时会接收任意数量的参数(通过 ...args)。
  • 在匿名函数内部,它首先将传入的参数数组 args 转换成一个字符串,用作缓存对象memo的键。这里使用 JSON.stringify 是为了确保即使是对象类型的参数也可以被唯一地表示为一个字符串。
  • 接下来,匿名函数检查 memo 是否已经包含了对应的键(即是否已经缓存了该参数组合的调用结果)。
  • 如果 memo 中没有这个键,说明这个参数组合的函数调用结果还没有被缓存,于是它调用原始函数 func 并传入参数,然后将结果存储在 memo 中,键为之前生成的字符串。
  • 如果 memo 中已经有了这个键,说明之前已经计算过这个参数组合的结果,直接从 memo 中返回缓存的结果,而不需要再次调用原始函数。

通过这种方式,缓存装饰器可以显著提高那些计算成本高昂且具有相同参数组合的函数的效率,因为它避免了重复计算。

我们可以直观地看一下加了和不加的区别:

  • 加了

    我所知道的JavaScript——实用的数组小技巧分享

  • 没加

    我所知道的JavaScript——实用的数组小技巧分享

为什么这里的闭包能够生效?

感兴趣的掘友可以查看【拓展章节中的详细解释。

拓展

在这一章节,我们会深入一下知识点背后的原理,尽可能地理解并掌握。

(这个过程也是我自己不断学习提升的过程,因此如果存在纰漏,欢迎批评指正。)

为什么 Array()new Array() 是等价的

浅谈this指向

我们通过阅读ECMAScript规范,再次确认了2种方式的等价性,接下来我们通过临时回忆一下JavaScript中的this,来尝试解释等价性

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。 ——《你不知道的JavaScript》

要明确this到底指向谁,有2个关键步骤要做:

  • 确定函数的调用位置
  • 明确this的绑定规则

在本篇文章,我们先略过通过分析调用栈来确定函数的调用位置,以及默认绑定、隐式绑定、显式绑定等绑定规则,直接来聊聊new绑定。

(有关this的指向问题,后续我会更新一篇更详细的文章。挖坑大王

在传统的面向对象编程的语言中(比如Java),创建某个类的实例化对象时,往往会用上new操作符,通过new创建对象的同时,还会调用类的构造函数,完成对对象的初始化操作,下面是一个代码例子:

public class Box{
   public Box(String company){
      //这个构造器仅有一个参数:company
      System.out.println("盒子的厂商是 : " + company ); 
   }
   public static void main(String[] args){
      // 下面的语句将创建一个Box对象
      Box myBox = new Box( "Juejin" );
   }
}
// 运行后将打印如下结果
// 盒子的厂商是 : Juejin

Javascript中,我们也有new这个操作符,也会使用new去创建类的实例化对象,比如:

let array1 = new Array()

但是这并不意味着Javascript中实际上存在“类”。

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。Javascript也有类似的语法(比如:new,instanceof),但是和其他语言中的类完全不同。——《你不知道的Javascript》

Javascript中,我们需要转变对“构造函数”的定义,我们应将其看成“只是一些使用new操作符时被调用的函数”。

Javascript中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。——《你不知道的Javascript》

将上面这段从英译中之后,再中译中一下,我们可以这样理解:

所谓的“构造函数”并没有任何的特殊性,我们不妨将它们全都一视同仁。而真正有特殊性的是对它们的调用操作

试想现在有这么一个函数getSum:

const getSum  = (a,b) => a + b

如果我们要计算两数之和了,我们就会这样去调用这个函数

const sum = getSum(1,2)

👆这是很普通的一次调用。

而当我们使用new操作符对Array()进行这样的调用时

const array1 = new Array()

👆这就是一次“构造调用

我们可以得出这样一个结论:

实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

现在我们来看看使用new操作符完成一次对函数的构造调用时,会发生什么事吧:

  1. 创建一个全新的对象。
  2. 这个全新的对象会被执行[[Prototype]]连接。(这里是有关原型的知识点了。)
  3. 这个全新的对象会被绑定到函数调用的this。(这就是所谓的new绑定。)
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回在第1步时创建的这个对象。

为什么我要大费周章地讲述thisnew绑定规则呢?

我们将刚刚头脑风暴后的思绪收回,回到本小节的主题上,在本小节我们要解决的问题是:

为什么省略new和加上new去调用Array(),都能成功创建一个数组?

v8引擎源码

当我们在聊JavaScript的时候,对应的引擎往往是指v8引擎。而v8引擎是基于c++语言实现的,也就是这里的Array()真要看源码的话,都是c++版本的源码。

我们可以从v8引擎的官方文档v8: Array Class Reference (nodesource.com)找到与Array相关的描述。

我所知道的JavaScript——实用的数组小技巧分享

我所知道的JavaScript——实用的数组小技巧分享

我所知道的JavaScript——实用的数组小技巧分享 👆上图展示的都是声明文件的内容。

我们去v8引擎的代码里找一找有关Array的实现。 我们可以在v8-container.h文件里找到有关Array的声明

我所知道的JavaScript——实用的数组小技巧分享

然后我们可以在api.cc中找到刚刚声明文件中的New函数的完整的代码:

Local<v8::Array> v8::Array::New(Isolate* v8_isolate, int length) {
  i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
  API_RCS_SCOPE(i_isolate, Array, New);
  ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
  int real_length = length > 0 ? length : 0;
  i::Handle<i::JSArray> obj = i_isolate->factory()->NewJSArray(real_length);
  i::DirectHandle<i::Number> length_obj =
      i_isolate->factory()->NewNumberFromInt(real_length);
  obj->set_length(*length_obj);
  return Utils::ToLocal(obj);
}
  1. 这个函数接受一个Isolate*对象和一个整数length作为参数。
  2. 它首先将length转换为非负数(如果length为负数,则转换为0)。
  3. 然后,使用Isolate对象的factory创建一个新的JSArray实例。
  4. 最后,将创建的JSArray实例转换为Local<Array>对象,并返回。

接下来,我们去看一下factory()->NewJSArray的代码,我们可以在这个文件中factory.cc找到:

Handle<JSArray> Factory::NewJSArray(ElementsKind elements_kind, int length,
                                    int capacity,
                                    ArrayStorageAllocationMode mode,
                                    AllocationType allocation) {
  DCHECK(capacity >= length);
  if (capacity == 0) {
    return NewJSArrayWithElements(empty_fixed_array(), elements_kind, length,
                                  allocation);
  }

  HandleScope inner_scope(isolate());
  DirectHandle<FixedArrayBase> elms =
      NewJSArrayStorage(elements_kind, capacity, mode);
  return inner_scope.CloseAndEscape(NewJSArrayWithUnverifiedElements(
      elms, elements_kind, length, allocation));
}

这个函数用于创建一个新的JSArray实例。

  1. 参数验证:首先,通过DCHECK(capacity >= length)进行断言,确保提供的容量至少与长度相等。如果容量小于长度,函数将不会继续执行。

  2. 特殊情况处理:如果容量为0,说明不需要创建任何元素,因此函数会使用NewJSArrayWithElements方法创建一个新的JSArray实例,并使用一个内部方法empty_fixed_array()来获取一个空的FixedArray实例作为数组的元素。

  3. 创建数组存储:如果容量不为0,函数会创建一个新的JSArrayStorage实例。这将涉及到以下步骤:

    • 使用inner_scope创建一个局部作用域,以避免垃圾收集。

    • 调用NewJSArrayStorage方法,这是一个内部方法,用于创建实际的数组存储。这个方法接受几个参数:

      • elements_kind:数组元素的种类,这决定了数组存储的结构。
      • capacity:数组的容量。
      • mode:一个枚举类型,用于指示如何��配数组存储。这个枚举类型可能是ArrayStorageAllocationMode,但具体实现细节没有提供。
  4. 设置数组内容:一旦创建了新的数组存储,函数会使用NewJSArrayWithUnverifiedElements方法来创建实际的JSArray实例。这个方法是JSArray类的一个内部方法,用于创建数组实例。

这个函数的核心是创建和管理JSArrayStorage实例,这是V8引擎内部处理数组存储的一种机制。通过这个函数,V8可以高效地创建和管理JavaScript数组实例,同时确保内存的使用是合理和高效的。

最后的最后,我们再看一下NewJSArrayStorage的源码,可以看到源码里用了很多次函数重载(函数重载是C++的一种特性,允许在同一个作用域内定义多个具有相同名称但参数列表不同的函数。当调用函数时,编译器会根据传递的参数来确定调用哪个函数。),所以又增加了阅读上的难度。还是在factory.cc这个文件中:

Handle<JSArray> Factory::NewJSArrayWithUnverifiedElements(
    DirectHandle<FixedArrayBase> elements, ElementsKind elements_kind,
    int length, AllocationType allocation) {
  DCHECK(length <= elements->length());
  Tagged<NativeContext> native_context = isolate()->raw_native_context();
  Tagged<Map> map = native_context->GetInitialJSArrayMap(elements_kind);
  if (map.is_null()) {
    Tagged<JSFunction> array_function = native_context->array_function();
    map = array_function->initial_map();
  }
  return NewJSArrayWithUnverifiedElements(handle(map, isolate()), elements,
                                          length, allocation);
}

Handle<JSArray> Factory::NewJSArrayWithUnverifiedElements(
    DirectHandle<Map> map, DirectHandle<FixedArrayBase> elements, int length,
    AllocationType allocation) {
  auto array = Cast<JSArray>(NewJSObjectFromMap(map, allocation));
  DisallowGarbageCollection no_gc;
  Tagged<JSArray> raw = *array;
  raw->set_elements(*elements);
  raw->set_length(Smi::FromInt(length));
  return array;
}

我们逐一分析这两个函数,因为我们可以看到在第1个函数中,return了第2个函数。而在第2个函数中,才真正return了array。

第一个NewJSArrayWithUnverifiedElements

它用于创建一个新的JSArray实例,并使用给定的FixedArrayBase作为数组的元素。

函数的实现包括以下步骤:

  1. 参数验证:首先,通过DCHECK(length <= elements->length())进行断言,确保传递的length值不会超过elements数组的长度。

  2. 获取初始的JSArray Map

    • 使用isolate()->raw_native_context()获取当前的NativeContext实例。
    • 调用native_context->GetInitialJSArrayMap(elements_kind)获取与给定ElementsKind相对应的初始JSArray映射(Map)。
    • 如果获取的Mapnull,说明没有预定义的映射,于是调用native_context->array_function()->initial_map()来获取默认的JSArray映射。
  3. 创建JSArray实例

    • 使用handle(map, isolate())创建一个Handle<Map>对象,用于在isolate中引用Map
    • 调用NewJSArrayWithUnverifiedElements(handle(map, isolate()), elements, length, allocation)来创建实际的JSArray实例。

第二个NewJSArrayWithUnverifiedElements

它用于创建一个新的JSArray实例,并使用给定的MapFixedArrayBase作为数组的元素。

函数的实现包括以下步骤:

  1. 创建JSArray实例

    • 使用NewJSObjectFromMap(map, allocation)创建一个新的JavaScript对象实例,这个实例是一个JSArray
    • 通过Cast<JSArray>将创建的对象转换为JSArray类型。
  2. 设置数组元素和长度

    • 使用DisallowGarbageCollection no_gc;创建一个临时的垃圾收集禁止范围,以避免在设置数组元素和长度时进行垃圾收集。
    • 通过raw->set_elements(*elements)设置数组的元素为给定的FixedArrayBase实例。
    • 通过raw->set_length(Smi::FromInt(length))设置数组的长度为给定的length值。
  3. 返回JSArray实例

    • 返回创建的JSArray实例。

通过看完上述这么多的c++源码,和new操作符是否存在相关的处理逻辑近似的就是下方这一块内容👇

我所知道的JavaScript——实用的数组小技巧分享

这已经是我本人出于自己的能力,对于v8引擎底层实现Array()的原理所能探究的全部了。

(如果你有更深的理解和看法,欢迎分享。)

模拟实现

我们不妨使用JavaScript并结合new绑定去模拟一下Array()函数的内部实现。

假设Array()函数存在一个内部实现的逻辑,如果它检测到自己没有被new操作符以构造调用的方式调用时,它会自动创建一个新的用来绑定到this上的对象,等同于隐式调用了new

将上面的描述文字转换成代码的形式展示,则长这样(非常简化的一次对内部实现的模拟):

function Array() {
  if (!(this instanceof Array)) {
    return new Array(...arguments);
  }
  // 初始化数组的代码...
}

没错,我之所以会在前面的内容讲到this指向,就是为了让大家能够更好地理解模拟代码中的这一行条件判断:

if(!(this instanceof Array))

如果我们是使用new操作符对Array()函数发起一次构造调用,则会一个全新的Array对象(new操作符创建的)被绑定Array()函数中的this上。于是

!(this instanceof Array)

的结果就是false,条件语句判断失败,无需隐式创建。

反之,当我们不使用new操作符,而是对Array()函数发起一次普通的调用时,此时并没有一个全新的Array对象被绑定Array()函数中的this上(此时this指向全局对象,【严格模式】下指向undefined),于是

!(this instanceof Array)

的结果就是true。则条件语句判断成功,应用内部逻辑。

 return new Array(...arguments);

隐式地用new操作符完成一次构造调用

通过我们的模拟实现,我们也能够做到Array()函数在构造调用和普通调用时,获得一样的结果。

记忆化搜索

我所知道的JavaScript——实用的数组小技巧分享 图文题解

产生闭包的分析

// 使用闭包和高阶函数来实现缓存装饰器
function cache(func) {
  const memo = new Map();

  return function(...args) {
    const key = JSON.stringify(args);
    if (!memo.has(key)) {
      memo.set(key, func.apply(this, args));
    }
    return memo.get(key);
  };
}

cache这个函数中,闭包之所以生效,是因为返回的匿名函数能够保持对其外部作用域(即 cache 函数的作用域)的引用。闭包的关键特点在于:

  • 内部函数可以访问定义它们的外部函数的作用域中的变量
  • 即使外部函数已经执行完毕,内部函数仍然可以访问那些外部变量

接下来我们一步一步仔细地展开聊聊:

  1. 内部函数:在 cache 函数内部定义的匿名函数就是一个内部函数。这个内部函数在 cache 函数执行时被创建,并且每次 cache 被调用时都会返回一个新的内部函数实例。
  2. 外部变量引用:内部函数引用了 cache 函数作用域中的变量 memo 和 funcmemo 是一个 Map 对象,用于存储函数调用的结果,而 func 是被装饰的原始函数。
  3. 保持作用域:当 cache 函数执行完毕后,通常其作用域会被销毁,其中的变量也会随之消失。但由于内部函数(闭包)保持了对外部作用域的引用,memo 和 func 变量仍然被保留在内存中。这意味着,即使 cache 函数的执行上下文已经消失,返回的内部函数仍然可以访问和修改 memo
  4. 持续存在:由于闭包的特性,memo 变量在内部函数的生命周期内持续存在,因此可以用来存储和检索函数调用的结果。每次调用返回的内部函数时,它都会检查 memo,以确定是否已经计算过相同参数的结果。

总结一下,闭包之所以生效,是因为内部函数能够访问并保持对其外部作用域中变量的引用,这使得 memo 变量可以在多次函数调用之间持续存在,从而实现了缓存功能。

结语

原本,我的初衷是撰写一篇简洁明了的实用小技巧分享文章,希望能够直接、高效地传递知识,让掘友们在短时间内掌握要点。

然而,在思考和整理这些小技巧的过程中,我发现许多细节和背后的原理也是同样重要的,不禁又洋洋洒洒地展开,最终形成了一篇长篇大论。

没办法,个人写作风格如此。

“知其然,知其所以然”一直是我撰写技术分享文章的宗旨。我不仅仅满足于告诉大家怎么做,更希望大家能够理解为什么这样做,以及背后的逻辑和原理。

或许这样的文章篇幅较长,但我相信对于掘友们理解和应用来说,这样的深度和广度是值得的。

最后,感谢耐心阅读至此的你,希望我的分享能为你带来实质性的帮助和启发。

(如果有任何的意见和建议,欢迎评论区留言或者直接私信我,我们下一篇文章见~)

我所知道的JavaScript——实用的数组小技巧分享

转载自:https://juejin.cn/post/7391034124372869135
评论
请登录