WebSocket

一、什么是WebSocket WebSocket 是一种在单个TCP连接上进行全双工通信的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。(维基百科) WebSocket本质上一种计算机网络应用层的协议,用来弥补http协议在持久通信能力上的不足。 WebSocket 协议在2008年诞生,2011年成为国际标准。现在最新版本浏览器都已经支持了。 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 WebSocket 的其他特点包括: (1)建立在 TCP 协议之上,服务器端的实现比较容易。 (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 (3)数据格式比较轻量,性能开销小,通信高效。 (4)可以发送文本,也可以发送二进制数据。 (5)没有同源限制,客户端可以与任意服务器通信。 (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。 ws://example.com:80/some/path 为什么需要 WebSocket? 我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处? 因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不具备服务器推送能力。 举例来说,我们想了解查询今天的实时数据,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。 这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。 在 WebSocket 协议出现以前,创建一个和服务端进双通道通信的 web 应用,需要依赖HTTP协议,进行不停的轮询,这会导致一些问题: 服务端被迫维持来自每个客户端的大量不同的连接 大量的轮询请求会造成高开销,比如会带上多余的header,造成了无用的数据传输。 http协议本身是没有持久通信能力的,但是我们在实际的应用中,是很需要这种能力的,所以,为了解决这些问题,WebSocket协议由此而生,于2011年被IETF定为标准RFC6455,并被RFC7936所补充规范。 并且在HTML5标准中增加了有关WebSocket协议的相关api,所以只要实现了HTML5标准的客户端,就可以与支持WebSocket协议的服务器进行全双工的持久通信了。 WebSocket 与 HTTP 的区别 WebSocket 与 HTTP的关系图: 相同点: 都是一样基于TCP的,都是可靠性传输协议。都是应用层协议。 联系: WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。 下面一张图说明了 HTTP 与 WebSocket 的主要区别: ...

November 8, 2024

Service Worker

引言 我们经常可以听到 Worker 这个名词,比如本文要介绍的 ServiceWorker 后面就带着一个 Worker 的单词,那么 Worker 实际上指的是什么呢? 在介绍 Worker 的定义之前,先来回忆一下浏览器的渲染原理。 在一个 tab 打开的时候,浏览器会给这个 tab 创建一个新的渲染进程(renderer process),如下图: 图1:Chrome 浏览器的多进程架构 图片来源:developer.chrome.com/blog/inside… 在每一个渲染进程中,都会有一个主线程(Main Thread),负责 JavaScript 的执行以及浏览器的渲染(JavaScript 的执行与 UI 渲染是一个互相阻塞的流程)。 图2:浏览器一帧的剖析 图片来源:aerotwist.com/blog/the-an… 如果 JavaScript 执行时间过久(比如超过 33.33 ms),那么一帧内留给 UI 渲染的时间不多,如果这时候网页有正在执行的动画,那么用户就会感受到卡顿。 10 FPS:能够达成基本的视觉残留(参考:定格动画至少需要 12 帧每秒以上) 30 FPS以下:让人感觉到明显的卡顿和不适感 30-50 FPS:因人而异 50-60 FPS:流畅 一个 Worker,指的是一个可以在后台独立执行 JavaScript 脚本。它存在于一个单独的 worker 线程,即使执行一些长任务也不会阻塞主线程响应用户操作(如鼠标点击、动画等)。 另外 Worker 也指使用 new Worker() 构造函数创建的一个对象,可以用在主线程与 worker 线程的通信 图3:worker 线程独立于主线程之外 图片来源:developer.chrome.com/blog/inside… Worker 与 主线程之间可以通过 postMessage 进行通信: ...

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. 获取字符串长度 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中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。 前言 说到深浅拷贝,必须先提到的是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

全方位解读this

我们在学习JavaScript的过程中,由于对一些概念理解得不是很清楚,但是又想要通过一些方式把它记下来,于是就很容易草率的给这些概念定下一些方便自己记忆的有偏差的结论。 危害比较大的是,有的不准确的结论在网上还广为流传。 比如对于this指向的理解中,有这样一种说法:谁调用它,this就指向谁。在我刚开始学习this的时候,我是非常相信这句话的。因为在一些情况下,这样理解也还算说得通。可是我常常会在开发中遇到一些不一样的情况,一个由于this的错误调用,可以让我懵逼一整天。那个时候我也查资料,在群里问大神,可是我仍然搞不清楚“我特么到底错哪里了”。其实只是因为我心中有一个不太准确的结论。 所以,我认为需要有这样一篇文章,来帮助大家全方位的解读this。让大家对this,有一个正确的,全面的认知。 在这之前,我们需要来回顾一下执行上下文。 在前面的几篇文章中,我有好几个地方都提到执行上下文的生命周期,为了防止大家没有记住,再次来回顾一下,如下图。 在执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象与作用域链我们都已经仔细总结过了,而这里的关键,就是确定this指向。 首先我们需要得出一个非常重要一定要牢记于心的结论,**this的指向,是在函数被调用的时候确定的。**也就是执行上下文被创建时确定的。因此,一个函数中的this指向,可以是非常灵活的。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象。 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn(); // 10 fn.call(obj); // 20 除此之外,在函数执行过程中,this一旦被确定,就不可更改了。 var a = 10; var obj = { a: 20 } function fn () { this = obj; // 这句话试图修改this,运行后会报错 console.log(this.a); } fn(); 一、全局对象中的this 关于全局对象的this,我之前在总结变量对象的时候提到过,它是一个比较特殊的存在。全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。 // 通过this绑定到全局对象 this.a2 = 20; // 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身 var a1 = 10; // 仅仅只有赋值操作,标识符会隐式绑定到全局对象 a3 = 30; // 输出结果会全部符合预期 console.log(a1); console.log(a2); console.log(a3); 二、函数中的this 在总结函数中this指向之前,我想我们有必要通过一些奇怪的例子,来感受一下函数中this的捉摸不定。 // demo01 var a = 20; function fn() { console.log(this.a); } fn(); // demo02 var a = 20; function fn() { function foo() { console.log(this.a); } foo(); } fn(); // demo03 var a = 20; var obj = { a: 10, c: this.a + 20, fn: function () { return this.a; } } console.log(obj.c); console.log(obj.fn()); 这几个例子需要花点时间仔细感受一下,如果你暂时没想明白怎么回事,也不用着急,我们一点一点来分析。 ...

November 6, 2024

变量对象

这篇文章要给大家介绍的是变量对象。 在JavaScript中,我们肯定不可避免的需要声明变量和函数,可是JS解析器是如何找到这些变量的呢?我们还得对执行上下文有一个进一步的了解。 在知识点《执行上下文》中,我们已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。 创建阶段 在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。 代码执行阶段 创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。 从这里我们就可以看出详细了解执行上下文极为重要,因为其中涉及到了变量对象,作用域链,this等很多人没有怎么弄明白,但是却极为重要的概念,它关系到我们能不能真正理解JavaScript。在后面的文章中我们会一一详细总结,这里我们先重点了解变量对象。 变量对象(Variable Object) 变量对象的创建,依次经历了以下几个过程。 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。 许多读者在阅读到这的时候会因为下面的这样场景对于“跳过”一词产生疑问。既然变量声明的foo遇到函数声明的foo会跳过,可是为什么最后foo的输出结果仍然是被覆盖了? function foo() { console.log('function foo') } var foo = 20; console.log(foo); // 20 其实只是大家在阅读的时候不够仔细,因为上面的三条规则仅仅适用于变量对象的创建过程。也就是执行上下文的创建过程。而foo = 20是在执行上下文的执行过程中运行的,输出结果自然会是20。对比下例。 console.log(foo); // function foo function foo() { console.log('function foo') } var foo = 20; // 上栗的执行顺序为 // 首先将所有函数声明放入变量对象中 function foo() { console.log('function foo') } // 其次将所有变量声明放入变量对象中,但是因为foo已经存在同名函数,因此此时会跳过undefined的赋值 // var foo = undefined; // 然后开始执行阶段代码的执行 console.log(foo); // function foo foo = 20; 根据这个规则,理解变量提升就变得十分简单了。在很多文章中虽然提到了变量提升,但是具体是怎么回事还真的很多人都说不出来,以后在面试中用变量对象的创建过程跟面试官解释变量提升,保证瞬间提升逼格。 在上面的规则中我们看出,function声明会比var声明优先级更高一点。为了帮助大家更好的理解变量对象,我们结合一些简单的例子来进行探讨。 // demo01 function test() { console.log(a); console.log(foo()); var a = 1; function foo() { return 2; } } test(); 在上例中,我们直接从test()的执行上下文开始理解。全局作用域中运行test()时,test()的执行上下文开始创建。为了便于理解,我们用如下的形式来表示 ...

November 6, 2024

执行上下文

我们在JS学习初期或者面试的时候常常会遇到考核变量提升的思考题。比如先来一个简单一点的。 console.log(a); // 这里会打印出什么? var a = 20; 暂时先不管这个例子,我们先引入一个JavaScript中最基础,但同时也是最重要的一个概念执行上下文(Execution Context)。 每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。JavaScript中的运行环境大概包括三种情况。 全局环境:JavaScript代码运行起来会首先进入该环境 函数环境:当函数被调用执行时,会进入当前函数中执行代码 eval(不建议使用,可忽略) 因此在一个JavaScript程序中,必定会产生多个执行上下文,在我的上一篇文章中也有提到,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。 当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。为了更加清晰的理解这个过程,根据下面的例子,结合图示给大家展示。 执行上下文可以理解为函数执行的环境,每一个函数执行时,都会给对应的函数创建这样一个执行环境。 var color = 'blue'; function changeColor() { var anotherColor = 'red'; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor(); 我们用ECStack来表示处理执行上下文组的堆栈。我们很容易知道,第一步,首先是全局上下文入栈。 全局上下文入栈之后,其中的可执行代码开始执行,直到遇到了changeColor() ,这一句激活函数changeColor创建它自己的执行上下文,因此第二步就是changeColor的执行上下文入栈。 changeColor的上下文入栈之后,控制器开始执行其中的可执行代码,遇到swapColors()之后又激活了一个执行上下文。因此第三步是swapColors的执行上下文入栈。 在swapColors的可执行代码中,再没有遇到其他能生成执行上下文的情况,因此这段代码顺利执行完毕,swapColors的上下文从栈中弹出。 swapColors的执行上下文弹出之后,继续执行changeColor的可执行代码,也没有再遇到其他执行上下文,顺利执行完毕之后弹出。这样,ECStack中就只身下全局上下文了。 全局上下文在浏览器窗口关闭后出栈。 注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。 详细了解了这个过程之后,我们就可以对执行上下文总结一些结论了。 单线程 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待 全局上下文只有唯一的一个,它在浏览器关闭时出栈 函数的执行上下文的个数没有限制 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。 为了巩固一下执行上下文的理解,我们再来绘制一个例子的演变过程,这是一个简单的闭包例子。 function f1(){ var n=999; function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到result执行时,才创建了一个新的。具体演变过程如下。 最后留一个简单的例子,大家可以自己脑补一下这个例子在执行过程中执行上下文的变化情况。 var name = "window"; var p = { name: 'Perter', getName: function() { // 利用变量保存的方式保证其访问的是p对象 var self = this; return function() { return self.name; } } } var getName = p.getName(); var _name = getName(); console.log(_name); 常见考点 执行上下文的定义 什么是执行上下文?它在 JavaScript 中有哪些类型? 执行上下文在代码执行时是如何被创建和销毁的? 执行上下文的生命周期 执行上下文的创建和执行过程有哪些阶段?(例如,创建阶段和执行阶段) 请描述执行上下文在创建阶段进行的三个步骤:变量对象的创建、作用域链的建立、this 的绑定。 变量对象(Variable Object) 什么是变量对象?它在执行上下文中的作用是什么? 变量提升是如何与变量对象关联的?在函数声明和变量声明时有何不同? 作用域链 执行上下文是如何通过作用域链来访问变量的? 请解释在嵌套函数的情况下,作用域链是如何建立的? this 绑定 在不同的执行上下文中,this 的值是如何确定的?(例如,全局上下文、函数上下文、箭头函数) 如何解释在不同函数调用方式下 this 的指向差异? 执行上下文栈 什么是执行上下文栈?它如何管理代码执行的顺序? JavaScript 引擎如何利用执行上下文栈来实现函数的嵌套调用和返回? 块级作用域的影响 ES6 中引入的 let 和 const 是如何影响执行上下文的? 如何解释块级作用域在执行上下文中的特殊处理? 常见问题与调试 如何通过理解执行上下文来排查变量未定义、this 指向错误等常见问题? 请解释某个你遇到的实际问题,以及如何通过分析执行上下文来解决它的。

November 6, 2024

内存空间

var a = 20; var b = 'abc'; var c = true; var d = { m: 20 } 因为JavaScript具有自动垃圾回收机制,所以对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不是计算机专业的朋友在进入到前端之后,会对内存空间的认知比较模糊,甚至有些人干脆就是一无所知。 当然也包括我自己。在很长一段时间里认为内存空间的概念在JS的学习中并不是那么重要。可是后我当我回过头来重新整理JS基础时,发现由于对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。 因此后来我才渐渐明白,想要对JS的理解更加深刻,就必须对内存空间有一个清晰的认知。 在学习内存空间之前,我们需要对三种数据结构有一个直观的认知。他们分别是堆(heap),栈(stack)与队列(queue)。 一、栈数据结构 与C/C++不同,JavaScript中并没有严格意义上区分栈内存与堆内存。因此我们可以简单粗暴的理解为JavaScript的所有数据都保存在堆内存中。但是在某些场景,我们仍然需要基于堆栈数据结构的思维来实现一些功能,比如JavaScript的执行上下文(关于执行上下文我会在下一篇文章中总结)。执行上下文的执行顺序借用了栈数据结构的存取方式。(也就是后面我们会经常提到的函数调用栈)。因此理解栈数据结构的原理与特点十分重要。 要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图左侧。 这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。图中已经详细的表明了栈空间的存储原理。 二、堆数据结构 堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。 书虽然也整齐的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。 三、队列 在JavaScript中,理解队列数据结构的目的主要是为了清晰的明白事件循环(Event Loop)的机制到底是怎么回事。在后续的章节中我会详细分析事件循环机制。 队列是一种先进先出(FIFO)的数据结构。正如排队过安检一样,排在队伍前面的人一定是最先过检的人。用以下的图示可以清楚的理解队列的原理。 四、变量对象与基础数据类型 JavaScript的执行上下文生成之后,会创建一个叫做变量对象的特殊对象(具体会在下一篇文章与执行上下文一起总结),JavaScript的基础数据类型往往都会保存在变量对象中。 严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。 基础数据类型都是一些简单的数据段,JavaScript中有5中基础数据类型,分别是Undefined、Null、Boolean、Number、String。基础数据类型都是按值访问,因为我们可以直接操作保存在变量中的实际的值。 ES6中新加了一种基础数据类型Symbol,可以先不用考虑他 五、引用数据类型与堆内存 与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。 为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。 var a1 = 0; // 变量对象 var a2 = 'this is string'; // 变量对象 var a3 = null; // 变量对象 var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中 var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中 因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。 在前端面试中我们常常会遇到这样一个类似的题目 // demo01.js var a = 20; var b = a; b = 30; // 这时a的值是多少? // demo02.js var m = { a: 10, b: 20 } var n = m; n.a = 15; // 这时m.a的值是多少 在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。 ...

November 6, 2024

数据类型转换

前言 JavaScript 是一种非常灵活的编程语言,它允许开发者在代码中进行各种类型的操作和转换。其中,显式和隐式类型转换是 JavaScript 中一个重要而又常见的概念。你是否曾经遇到过 JavaScript 中奇怪的类型转换行为?当你使用加号连接两个不同类型的值时,为什么有时候会得到意料之外的结果?为了更好地理解 JavaScript 中的这些行为,我本篇文章将带大家深入探讨显式和隐式类型转换。 数据类型 首先简单了解下有哪些数据类型。 原始数据类型:String、Number、Boolean、Symbol、Undefined、Null、BigInt(本文不讨论BigInt、Symbol) 引用数据类型: Object(引用类型指向一个对象,不是原始值,指向对象的变量是引用变量) 类型转换 数据类型之间的转换可以分为下面三种: 原始数据类型之间的转换。 原始数据类型转换为对象。 对象转换为原始数据类型。 1.1 原始数据类型转Number console.log(Number('123')); // 123 console.log(Number(true)); // 1 console.log(Number(undefined)); // NaN console.log(Number(null)); // 0 我们可以直接调用Number()函数来进行转换。该函数会把undefined转换为NaN,将null转换为0。 而对于字符串,如果字符串中仅仅包含数字,则将转换为数字,若包含非数字字符,则返回NaN。我们也可以使用parseInt()来转换字符串。 console.log(parseInt('123')); // 123 console.log(parseInt('12.3')); // 12 console.log(parseInt('12ds8'));// 12 console.log(parseInt('ads25')); // NaN parseInt()只解析整数部分,忽略小数部分。parseFloat()可以将字符串转换为浮点数的函数。 1.2 原始数据类型转String console.log(String());//'' console.log(String(123));//'123' console.log(String(NaN));//NaN console.log(String(undefined));//undefined console.log(String(null));//null console.log(String(true));//true 我们可以使用String() 构造函数将任何类型的数据转换为字符串类型。它将接收到的参数转换为对应的字符串表示形式。 1.3 原始数据类型转Boolean console.log(Boolean());// false console.log(Boolean('Ywis'));// tru console.log(Boolean(123));// true console.log(Boolean(NaN));// false console.log(Boolean(undefined));// false console.log(Boolean(null));// false console.log(Boolean(true));// true 调用Boolean()函数进行转换。在将非布尔值类型转换为布尔值类型时,有一个总结: false:布尔值 false undefined:未定义的值 null:空值 0:数字 0 -0:负零 NaN:非数字值 '':空字符串 除了以上七个值外,其他值(包括对象、数组、函数等)在转换为布尔值时都会返回 true。这种规则使得 JS 中的大多数值被视为真值(truthy)。 ...

November 4, 2024