数组扩展

ES6(ECMAScript 2015)为JavaScript数组引入了多项扩展功能,这些功能不仅简化了数组操作,还提高了代码的可读性和效率。本文将详细介绍ES6数组扩展的主要功能和用法。 一、扩展运算符(…) 扩展运算符可以用于数组的赋值、合并、解构赋值以及将字符串转为数组。 赋值数组: let arr1 = [100, 200, 300, [500]]; let arr2 = [...arr1]; // 浅拷贝 console.log(arr2 == arr1); // false arr1[0] = 400; console.log(arr2); // 不受影响 arr1[3][0] = 600; console.log(arr2); // 受影响(只拷贝了一层) 合并数组: console.log([...[1, 2, 3], ...[4, 5, 6]]); // [1, 2, 3, 4, 5, 6] 解构赋值: let [a, ...list] = [1, ...["a", "b"]]; console.log(a); // 1 console.log(list); // ["a", "b"] 将字符串转为数组: console.log([..."str"]); // ["s", "t", "r"] 二、Array.from() Array.from()方法用于将类数组对象或可迭代对象转换为真正的数组。 let arrLike = { "0": "内容", "1": "内容2", "2": "内容3", length: 3 }; console.log(Array.from(arrLike)); // ["内容", "内容2", "内容3"] 此外,Array.from()还可以接受一个回调函数,用于对数组中的每个元素进行处理。 let arr = Array.from({ length: 2 }, () => "jack"); console.log(arr); // ["jack", "jack"] 三、Array.of() Array.of()方法用于将一组值转换为数组,与Array构造函数的行为不同,Array.of()不会把一个参数作为数组的长度。 ...

October 2, 2024

垃圾回收

写在前面 我们知道垃圾回收机制是引擎来做的,JS引擎有很多种(各个浏览器都不同),其垃圾回收机制在一些细节及优化上略有不同,本文我们以一些通用的回收算法作为切入,再由 V8 引擎发展至今对该机制的优化为例(为什么以 V8 为例?因为它市场占有率大 😄 ),一步一步深入来助我们了解垃圾回收机制,因为只有真正了解垃圾回收机制,后面才能理解内存泄漏的问题以及手动预防和优化 JavaScript 是门魅力无限的语言,关于它的 GC(垃圾回收)方面,你了解多少呢?想来大部分人是因为面试才去看一些面试题从而了解的垃圾回收,那在正式开始之前,给大家列几个小问题,大家可以先想一下答案,带着问题及答案再去看文章,最后读完此文如果你的答案可以优化,即有收获 什么是垃圾回收机制? 垃圾是怎样产生的? 为什么要进行垃圾回收? 垃圾回收是怎样进行的? V8 引擎对垃圾回收进行了哪些优化? 当然,我们可不仅仅是为了面试,其目的是一次性彻底搞懂 GC! GC是什么 GC 即 Garbage Collection ,程序工作过程中会产生很多 垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的,因为他工作在引擎内部,所以对于我们前端来说,GC 过程是相对比较无感的,这一套引擎执行而对我们又相对无感的操作也就是常说的 垃圾回收机制 了 当然也不是所有语言都有 GC,一般的高级语言里面会自带 GC,比如 Java、Python、JavaScript 等,也有无 GC 的语言,比如 C、C++ 等,那这种就需要我们程序员手动管理内存了,相对比较麻烦 垃圾产生&为何回收 我们知道写代码时创建一个基本类型、对象、函数……都是需要占用内存的,但是我们并不关注这些,因为这是引擎为我们分配的,我们不需要显式手动的去分配内存 但是,你有没有想过,当我们不再需要某个东西时会发生什么?JavaScript 引擎又是如何发现并清理它的呢? 我们举个简单的例子 let test = { name: "isboyjc" }; test = [1,2,3,4,5] 如上所示,我们假设它是一个完整的程序代码 我们知道 JavaScript 的引用数据类型是保存在堆内存中的,然后在栈内存中保存一个对堆内存中实际对象的引用,所以,JavaScript 中对引用数据类型的操作都是操作对象的引用而不是实际的对象。可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的 那上面代码首先我们声明了一个变量 test,它引用了对象 {name: 'isboyjc'},接着我们把这个变量重新赋值了一个数组对象,也就变成了该变量引用了一个数组,那么之前的对象引用关系就没有了,如下图 没有了引用关系,也就是无用的对象,这个时候假如任由它搁置,一个两个还好,多了的话内存也会受不了,所以就需要被清理(回收) 用官方一点的话说,程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃 垃圾回收策略 在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收 至于如何回收,其实就是怎样发现这些不可达的对象(垃圾)它并给予清理的问题, JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存 ...

October 1, 2024

尾部优化

ES6 引入了尾调用优化(Tail Call Optimization,TCO)的规范,但它的实现依赖于 JavaScript 引擎的支持(比如 V8 并未开启 TCO),所以这是一个理论上重要、实践中需要谨慎对待的考点。 一、什么是尾调用(Tail Call)? 尾调用是指: 函数的最后一步是调用另一个函数,并直接返回这个函数的执行结果。 例如: function foo() { return bar(); // bar 是 foo 的尾调用 } 尾调用意味着: 当前函数的调用栈帧不需要再做任何事。 调用栈可以复用,不需要为当前函数保留栈帧,从而节省内存。 二、什么是尾调用优化(TCO)? 尾调用优化的目标是: 当一个函数的最后一步是调用另一个函数,JS 引擎可以不再保留当前函数的调用记录,直接跳转执行被调用函数,复用当前栈帧。 也就是说: 不会形成函数调用嵌套的“栈帧链”; 可以避免函数递归导致的栈溢出(stack overflow)。 三、尾调用成立的条件 必须同时满足以下三个条件: 条件 说明 1. 在严格模式下 TCO 只在 strict mode 下才可能被引擎开启 2. 是函数的最后一步调用 return 后没有任何操作 3. 返回调用结果 return bar(x) 形式,不能是 bar(x); return y ✅ 尾调用示例: 'use strict'; function f(x) { return g(x); // 最后一步是返回 g(x),符合尾调用优化条件 } ❌ 非尾调用: function f(x) { const y = g(x); return y; // 不是直接返回函数调用 } 四、尾调用优化应用场景:尾递归 递归调用时,若能做到尾调用,就能避免栈溢出。 ...

October 2, 2024

DOM操作

1、DOM基本概念 1.1、DOM、DOM节点与DOM树 DOM:(Document Object Model, 文档对象模型)是文档内容(HTML或XML)在编程语言上的抽象模型,它建模了文档的内容和结构,并提供给编程语言一套完整的操纵文档的API。 本文面向HTML,不讨论XML等文档,下文中“文档===HTML”。 DOM节点:简称节点(Node),是DOM模型的组成单元。HTML的基本单元是标签,节点常常与标签对应,但连续的文本内容也是一个文本标签。 DOM树:DOM树是DOM结构的表示形式,DOM把文档的每个节点根据父子关系连接,形成DOM树。 1.2、节点、节点类型和节点类 节点:前面提过,节点是DOM树的组成单元。在JS看来,一个节点就是JS对象。下面用node表示任意的节点。 节点类型:并非所有的节点都是一样的,DOM规定文档中有12种节点类型,分别用常量1 ~ 12(有与之对应的常量名称Node.XXX_NODE)表示,可以通过node.nodeType属性获取节点的类型常量。 现在,有些类型的节点已经弃用了,常见到的只有几种类型的节点,包括: 元素节点: 类型常量为Node.ELEMENT_NODE或1。最常见的一类节点,对应文档中的元素。大部分DOM操作都是在元素节点层次的。 文本节点:类型常量为Node.TEXT_NODE或2。对应文档中的文本,任何文档内容都有对应的文本节点,即使空格和换行符。 空格和换行不会对页面内容产生影响,但它们确实以文本节点的形式存在于DOM树中。 Document节点:类型常量为Node.DOCUMENT_NODE或9。它不对应文档的内容,而是作为文档的入口节点,每个文档都有且仅有一个入口,因为这种独特性,赋予一个特殊的变量名称document。 注释节点:类型常量为Node.COMMENT_NODE或8。它对应文档中的注释标签,文档的注释内容也是可读取和修改的。 节点类:DOM内置许多节点类,类之间存在继承关系,形成一套节点类框架。每个节点对象都属于节点类,拥有该类和其父类的方法与属性,这使得操作节点十分简单。节点类框架的一部分大概如图: 这些节点上有丰富的属性和方法,是继承的结果,可以看到,一个HTML标签元素至少有四层的继承关系。 以<a>标签为例,它属于HTMLAnchorElement类,获得了a.target,a.download等属性,接着继承了HTMLElement类上的title, hidden等属性和click()等方法,又从Element类继承了tagName, className等属性和getAttribute(), setAttribute()等方法,再从Node类继承了nodeType(前面说过的节点类型), appenChild(), removeChild()等方法,最后从EventTarget类中继承了事件相关的属性和方法。 不要混淆节点类型和节点类这两个概念。前者是一个生活中的类别,后者是编程意义上的类。节点对象的nodeType属性表示了它的类型,而节点类是该节点的从属的类。因为Dode是一个抽象类,所以,如果知道了某个节点从属的类,我们就知道它的节点类型。 区分节点与元素节点。我们经常关心元素节点(简称元素),因为这是一类最常使用的节点,但是并非所有节点都是元素。 1.3、探索DOM结构 前面的说法太抽象了,让我们用实际的例子看看文档、DOM树与节点的关系。 Live DOM Viewer是一个可以根据HTML文档实时查看DOM树的网站。你把下面的例子复制过去,或者自己去探索。 一个简单的HTML文档: <!DOCTYPE HTML> <html> <body> A simple text. <ol title="this is a title"> <li>czpcalm</li> <!-- comment --> </ol> </body> </html> 它对应的DOM图(颜色区分了节点类型): 留意这个图,你要注意几点: 总共有4种类型的节点,分别是标签节点(红色),文本节点(灰色),注释节点(黄色)和DOCTYPE节点(紫色)。 文档没有<head>标签却出现了HEAD节点。这是因为HTML必然存在<html>,<head>,<body>标签,不存在时会自动补上。顺便一提,当出现<table>标签时,也一定会有<tbody>标签。 文档中的文本都会形成文本节点的内容,包括空格␣和换行↵。第一,单独的空格和换行都会形成对应的文本节点;第二,有内容的文本节点的值包含前导和后继的空白。 不是说HTML中的空白字符都被忽略吗?怎么这里又说全都是有效的字符? 在从文档解析生成DOM树的过程中,HTML中的任何字符都是有效的;不过,在接下去的页面渲染的过程中,空白内容被忽略。所以从文档到页面的整过过程中,空白确实被忽略了。 理解DOM树中的父子关系对应文档中的包含(嵌套)关系。一个极佳的类比是文件树,把元素看做文件夹,文本看做文件,文件夹中可以存放文件和新的文件夹,然后一层层深入下去,DOM树也是如此。 几个原则可以帮我们快速理解这个DOM树的构建: ...

October 1, 2024

事件冒泡、事件捕获、事件委托

DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。 Dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。 addEventListener的第三个参数 在我们平常用的addEventListener方法中,一般只会用到两个参数,一个是需要绑定的事件,另一个是触发事件后要执行的函数,然而addEventListener还可以传入第三个参数: element.addEventListener(event, function, useCapture); 第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。如果不写第三个参数则默认在事件冒泡阶段调用事件处理函数。 下面先介绍事件冒泡: 1. 事件冒泡 事件冒泡(dubbed bubbling):当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件,例如click、focus等等这些事件, 并不传递所绑定的事件函数。) 事件源 =>根节点(由内到外)进行事件传播。 举例说明: 给三个盒子依次绑定点击事件,当点击盒子的时候,会依次触发父级元素的点击事件。 click small box click center box click big box 如果父元素没有绑定点击事件则只会触发点击盒子的事件。 click small box 如果子元素(small)的点击事件去掉,当我们点击small的时候会把当前操作的点击事件传递给父元素(因为父元素绑定了点击函数) click small box 有些时候我们不希望产生事件冒泡,所以可以 在子事件中加入e.stopPropagation() 取消冒泡 click small box 2. 事件捕获 事件捕获(event capturing): 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。 事件捕获与事件冒泡是比较类似的,最大的不同在于事件传播的方向。 还是举上面的例子: click small box 3. 事件委托 事件委托也称为事件代理。就是利用事件冒泡,把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现。 原理实现: 不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,然后利用冒泡原理影响设置每个子节点。 应用场景:1000个button需要注册点击事件 如果循环给每个按钮添加点击事件,那么会增加内存损耗,影响性能 此时可以给button的父元素添加点击事件 这时相当于每个按钮都绑定了点击事件 优点: 替代循环绑定事件的操作,减少内存消耗,提高性能。比如: 在table上代理所有td的click事件。 在ul上代理所有li的click事件。 简化了dom节点更新时,相应事件的更新。比如: 不用在新添加的li上绑定click事件。 当删除某个li时,不用移解绑上面的click事件。 缺点: ...

October 1, 2024

call、apply和bind

apply 和 call简介 在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。 JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。 比如 A 对象有一个方法,而 B 对象因为某种原因,也需要用到同样的方法,那么这时候不用单独为 B 对象扩展一个方法,可以直接借用 A 对象的方法。这样既完成了需求,又减少了内存的占用。 直接来看个例子: function fruits() {} fruits.prototype = { color: "red", say: function() { console.log("My color is " + this.color); }} var apple = new fruits();apple.say(); //My color is red 但是如果我们有一个对象 banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法: banana = { color: "yellow"}apple.say.call(banana); //My color is yellowapple.say.apply(banana); //My color is yellow apply、call 的区别 对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下: ...

October 1, 2024

深拷贝/浅拷贝

浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。 前言 说到深浅拷贝,必须先提到的是JavaScript的数据类型,大家可以看前面的知识点,这里不多说了。 初学前端的同学,需要知道的一点就是:JavaScript的数据类型分为基本数据类型和引用数据类型。 对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。 浅拷贝 浅拷贝的意思就是只复制引用,而未复制真正的值。 const originArray = [1,2,3,4,5]; const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneArray = originArray; const cloneObj = originObj; console.log(cloneArray); // [1,2,3,4,5] console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}} cloneArray.push(6); cloneObj.a = {aa:'aa'}; console.log(cloneArray); // [1,2,3,4,5,6] console.log(originArray); // [1,2,3,4,5,6] console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}} console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}} 上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray 和 originObj 也随着发生了变化。 深拷贝 深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。 只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。 目前实现深拷贝的方法不多,主要是两种: 利用 JSON 对象中的 parse 和 stringify 利用递归来实现每一层都重新创建对象并赋值 JSON.stringify/parse的方法 先看看这两个方法吧: The JSON.stringify() method converts a JavaScript value to a JSON string. JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。 ...

November 7, 2024

字符串常见方法

1. 获取字符串长度 JavaScript中的字符串有一个length属性,该属性可以用来获取字符串的长度: const str = 'hello'; str.length // 输出结果:5 2. 获取字符串指定位置的值 charAt()和charCodeAt()方法都可以通过索引来获取指定位置的值: charAt() 方法获取到的是指定位置的字符; charCodeAt()方法获取的是指定位置字符的Unicode值。 (1)charAt() charAt() 方法可以返回指定位置的字符。其语法如下: string.charAt(index) index表示字符在字符串中的索引值: const str = 'hello'; str.charAt(1) // 输出结果:e 我们知道,字符串也可以通过索引值来直接获取对应字符,那它和charAt()有什么区别呢?来看例子: const str = 'hello'; str.charAt(1) // 输出结果:e str[1] // 输出结果:e str.charAt(5) // 输出结果:'' str[5] // 输出结果:undefined 可以看到,当index的取值不在str的长度范围内时,str[index]会返回undefined,而charAt(index)会返回空字符串;除此之外,str[index]不兼容ie6-ie8,charAt(index)可以兼容。 (2)charCodeAt() charCodeAt():该方法会返回指定索引位置字符的 Unicode 值,返回值是 0 - 65535 之间的整数,表示给定索引处的 UTF-16 代码单元,如果指定位置没有字符,将返回 NaN: let str = "abcdefg"; console.log(str.charCodeAt(1)); // "b" --> 98 通过这个方法,可以获取字符串中指定Unicode编码值范围的字符。比如,数字0~9的Unicode编码范围是: 48~57,可以通过这个方法来筛选字符串中的数字,当然如果你更熟悉正则表达式,会更方便。 3. 检索字符串是否包含特定序列 这5个方法都可以用来检索一个字符串中是否包含特定的序列。其中前两个方法得到的指定元素的索引值,并且只会返回第一次匹配到的值的位置。后三个方法返回的是布尔值,表示是否匹配到指定的值。 注意:这5个方法都对大小写敏感! (1)indexOf() indexOf():查找某个字符,有则返回第一次匹配到的位置,否则返回-1,其语法如下: string.indexOf(searchvalue,fromindex) 该方法有两个参数: searchvalue:必需,规定需检索的字符串值; fromindex:可选的整数参数,规定在字符串中开始检索的位置。它的合法取值是 0 到 string.length - 1。如省略该,则从字符串的首字符开始检索。 let str = "abcdefgabc"; console.log(str.indexOf("a")); // 输出结果:0 console.log(str.indexOf("z")); // 输出结果:-1 console.log(str.indexOf("c", 4)) // 输出结果:9 (2)lastIndexOf() lastIndexOf():查找某个字符,有则返回最后一次匹配到的位置,否则返回-1 ...

November 8, 2024

数组常用方法

数组是 js 中最常用到的数据集合,其内置的方法有很多,熟练掌握这些方法,可以有效的提高我们的工作效率,同时对我们的代码质量也是有很大影响。 一、创建数组 1.使用数组字面量表示法 var arr4 = []; //创建一个空数组 var arr5 = [20]; // 创建一个包含1项数据为20的数组 var arr6 = ["lily","lucy","Tom"]; // 创建一个包含3个字符串的数组 2.使用 Array 构造函数 无参构造 var arr1 = new Array(); //创建一个空数组 带参构造 如果只传一个数值参数,则表示创建一个初始长度为指定数值的空数组 var arr2 = new Array(20); // 创建一个包含20项的数组 如果传入一个非数值的参数或者参数个数大于 1,则表示创建一个包含指定元素的数组 var arr3 = new Array("lily","lucy","Tom"); // 创建一个包含3个字符串的数组 var array4 = new Array('23'); // ["23"] 3.Array.of 方法创建数组(es6 新增) ES6 为数组新增创建方法的目的之一,是帮助开发者在使用 Array 构造器时避开 js 语言的一个怪异点。 Array.of()方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型。 let arr = Array.of(1, 2); console.log(arr.length);//2 let arr1 = Array.of(3); console.log(arr1.length);//1 console.log(arr1[0]);//3 let arr2 = Array.of('2'); console.log(arr2.length);//1 console.log(arr2[0]);//'2' 4.Array.from 方法创建数组(es6 新增) 在 js 中将非数组对象转换为真正的数组是非常麻烦的。在 ES6 中,将可迭代对象或者类数组对象作为第一个参数传入,Array.from()就能返回一个数组。 function arga(...args) { //...args剩余参数数组,由传递给函数的实际参数提供 let arg = Array.from(args); console.log(arg); } arga('arr1', 26, 'from'); // ['arr1',26,'from'] 映射转换 如果你想实行进一步的数组转换,你可以向 Array.from()方法传递一个映射用的函数作为第二个参数。此函数会将数组对象的每一个值转换为目标形式,并将其存储在目标数组的对应位置上。 ...

November 8, 2024

正则表达式

前言 正则表达式使用单个[字符串]来描述、匹配一系列匹配某个[句法规则]的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。 创建 1.构造函数创建 new RegExp(‘正则表达式’,‘修饰符’) var reg = new RegExp('hello','igm'); 2.直接直面量创建 /正则表达式/修饰符 var reg = /hello/igm; 字符分类 1.普通字符 字母、数字、下划线、汉字、没有特殊含义的符号(,;!@等) 实际上不是特殊字符的字符都是普通字符 2.特殊字符 \:将特殊字符转义成普通字符 3.模式修饰符 i:ignoreCase,匹配时忽视大小写 m:multiline,多行匹配 g:global,全局匹配 字面量创建正则时,模式修饰符写在一对反斜线后 实例方法 1.exec() 用来匹配字符串中符合正则表达式的字符串 如果匹配到,返回值是一个result数组: [匹配的内容,index: 在str中匹配的起始位置,input: 参数字符串,groups: undefined] 否则返回null var str = 'Hello world javascript hello'; var reg = new RegExp('hello','igm'); var res = reg.exec(str); console.log(res); 输出结果为: [ 'Hello', index: 0, input: 'hello world javascript hello', groups: undefined ] 2.test() 用来测试待检测的字符串中是否有可以匹配到正则表达式的字符串 如果有返回true,否则返回false var reg = /hello/; var str = 'hello world'; console.log(reg.test(str)); //true 3.toSting() toLocaleString() 把正则表达式的内容转化成字面量形式字符串/有本地特色的字符串 var reg = /hello/; var str = 'hello world'; console.log(reg.toString(),typeof reg.toString()); // /hello/ string console.log(reg.toLocaleString(),typeof reg.toLocaleString()); // /hello/ string 4.valueOf() 返回正则表达式本身 ...

October 1, 2024