Let/Const

Var 在 ES6 出现之前,必须使用 var 声明。但是,前端开发出现的一些问题与使用 var 声明的变量有关。这就是为什么必须要有新的方法来声明变量。首先,让我们在讨论这些问题之前更多地了解 var。 var 的作用域 作用域本质上是指变量/函数可供访问的范围。var可以在全局范围声明或函数/局部范围内声明。 当在最外层函数的外部声明var变量时,作用域是全局的。这意味着在最外层函数的外部用var声明的任何变量都可以在windows中使用。 当在函数中声明var时,作用域是局部的。这意味着它只能在函数内访问。 要进一步了解,请查看下面的示例。 var greeter = 'hey hi'; function newFunction() { var hello = 'hello'; } 这里,greeter是全局范围的,因为它存在于函数外部,而hello是函数范围的。因此,我们无法在函数外部访问变量hello。因此,如果我们这样做: var tester = 'hey hi'; function newFunction() { var hello = 'hello'; } console.log(hello); // error: hello is not defined 我们会收到错误消息,这是由于函数外部没有hello导致的。 var 变量可以重新声明和修改 这意味着我们可以在相同的作用域内执行下面的操作,并且不会出错 var greeter = 'hey hi'; var greeter = 'say Hello instead'; 又比如 var greeter = 'hey hi'; greeter = 'say Hello instead'; var 的变量提升 变量提升是 JavaScript 的一种机制:在执行代码之前,变量和函数声明会移至其作用域的顶部。这意味着如果我们这样做: console.log(greeter); var greeter = 'say hello'; 生面的代码会被解释为: var greeter; console.log(greeter); // greeter is undefined greeter = 'say hello'; 因此,将var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化. ...

October 2, 2024

数据类型

前言 请讲下 JavaScript 中的数据类型? 前端面试中,估计大家都被这么问过。 答:Javascript 中的数据类型包括原始类型和引用类型。其中原始类型包括 Null、Undefined、Boolean、String、Symbol、BigInt。引用类型指的是 Object。 没错,我也是这么回答的,只是这通常是第一个问题,由这个问题可以引出很多很多的问题,比如 Null 和 Undefined 有什么区别?前端的判空有哪些需要注意的? typeof null 为什么是 object? 为什么 ES6 要提出 Symbol? BigInt 解决了什么问题? 为什么 0.1 + 0.2 !== 0.3? 你如何解决这个问题? 如何判断一个值是数组? … 弱类型语言 因为 JavaScript 是弱类型语言或者说是动态语言。这意味着你不需要提前声明变量的类型,在程序运行的过程中,类型会被自动确定,也就是说你可以使用同一个变量保存不同类型的值 var foo = 42; // foo is a Number now foo = "bar"; // foo is a String now foo = true; // foo is a Boolean now 这一特性给我们带来便利的同时,也给我们带来了很多的类型错误。试想一下,假如 JS 说是强类型语言,那么各个类型之间没法转换,也就有了一层隔阂或者说一层保护,会不会更加好维护呢?——这或许就是 TypeScript 诞生的原因。 对 JavaScript 的数据类型掌握,是一个前端最基本的知识点 null 还是 undefined 定义 undefined 表示未定义的变量。null 值表示一个空对象指针。 ...

October 1, 2024

类型判断

JS中的类型判断也是咱的老朋友了,如果要你说两句,那便是啥玩意typeof、instanceof的,张口就来,但你真的掌握了每一种类型判断,并熟知其原理吗?就比如说Object.prototype.toString.call(),当面试官问其中的 call有什么作用时,咱们又该如何游说呢。 typeof原理 所有的类型判断里当属typeof最为经典,其最为出名之处就是这玩意不顶用,typeof居然会将null判定为object,但这到底是什么原因?实际上,这就是JS这门编程语言的官方团队造出来的一个bug。 let s = '123' let n = 123 let f = true let u = undefined let nu = null let sy = Symbol(123) let big = 1234n let obj = {} let arr = [] let fn = function(){} let date = new Date(); console.log(typeof(s)); // string console.log(typeof(n)); // number console.log(typeof(f)) // boolean console.log(typeof(u)) // undifined console.log(typeof(sy)) // symbol console.log(typeof(big)) // bigint console.log(typeof(nu)); //将null判断为object //以下为引用类型,除了function,一律被判断为object console.log(typeof(obj)) //object console.log(typeof(arr)) //object console.log(typeof(date)) //object // 所有引用类型只能判断function console.log(typeof(fn)) //function // 原因就是:typeof会把所有传进去的值都转成二进制,而当年JS制定的规则便是:原始类型的被转为二进制的前面三个值绝对不为零,而typeof会把前三位为零的类型全部认定为对象,null这个类型又是JS语言后来引入的。遵循其他语言的原则,JS语言将null的二进制值定为一长串的0,因此typeof在判断时会将其认定为object。 因此,面试官一般不会询问判断方法 typeof 的原理。 手写一个instanceof 相较于typeof,instanceof有其优越之处,也有其不足之处,比如,instanceof只能判断引用类型,并且是通过原型链查找来判断类型。 let s = '123' let n = 123 let f = true let u = undefined let nu = null let sy = Symbol(123) let big = 1234n let obj = {} let arr = [] let fn = function(){} let date = new Date(); console.log(s instanceof String); // false instanceof 不能判断原始类型 // 以下为引用类型 console.log(obj instanceof Object) ; // true console.log(arr instanceof Object); // true console.log(fn instanceof Function); // true console.log(date instanceof Date); // true 首先要知道每个类型的原型都不相同。 ...

November 4, 2024

模版字符串

未见过的函数用法: function fun(strings,params1){ console.log(strings); console.log(params1) } fun`params1 = ${333}` // 输出分别为 // ['params1 = ', ''] // 333 不知道各位在学习或开发中有没有遇到过这样的函数调用方式呢?秉承着我没遇到的东西就要发文章来分享分享的原则,经过我查阅资料并加以学习,就让本文来带大家了解一下JS中的标签函数吧。 模板字符串 标签函数的主要作用就是处理模板字符串,所以在了解标签函数之前先了解一下模板字符串(模板字面量)吧。 基本用法 模板字符串是ES6新增的使用模板字面量定义字符串的能力,用反引号```````` 进行分隔。与使用''或""的定义的字符串字面量不同是模板字面量保留换行字符,可以跨行定义字符串。 例如: let str1 = 'xxhh\n2233' let str2 = `xxhh 2233` console.log(str1 === str2) // 结果为true 要注意的是模板字符串中的所有字符都会包含在字符串中,特别在使用缩进的时候,缩进的空格都会包含在字符串中。 let str = ` <div> <h1>xxhh</h1> </div> ` console.log(str.length) // 结果为44,包含了缩进所产出的所有空格 字符串插值 在没有模板字符串时,我们通常会使用+加法运算符来进行字符串的拼接,但有了模板字符串后我们并可通过插值占位符${}来将各字面量、变量或表达式结果来进行拼接,这样可以更容易地创建复杂和动态的字符串,同时保持代码的可读性和简洁性。这是模板字符串最常用的一个特性。 let name = 'xxhh'; let str1 = "name:"+name; let str2 = `name:${name}`; console.log(str1,str2) // name:xxhh name:xxhh console.log(str1 === str2) // true 要注意的是使用字符串插值的所有插入的值都会使用toString()强制转型为字符串,而且任何JavaScript 表达式都可以用于插值。 ...

October 2, 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

默认参数

一、参数传递 代码语言:javascript function foo(a, b) { console.log([a, b]); } foo(1, 2); // 输出 [1, 2] 这个例子中,a 和 b 属于函数中的局部变量,只能在函数中访问。调用函数时,传递的数据会根据位置来匹配对应,分别赋值给 a 和 b。 创建函数时,function 函数名 后面括号中设定的参数被称为形参;调用函数时,函数名后面括号中传入的参数被称为实参。上面例子中,a 和 b 是形参,传入的 1 和 2 是实参。 因为形参是已声明的变量,所以不能再用 let 和 const 重复声明。 代码语言:javascript function foo(a, b) { let a = 1; // 报错,a 已声明 const b = 1; // 报错,b 已声明 } JavaScript 中所有函数传递都是按值传递的,不会按引用传递。所谓的值,就是指直接保存在变量上的值,如果把对象作为参数传递,那么这个值就是这个对象的引用,而不是对象本身。这里实际上是一个隐式的赋值过程,所以给函数传递参数时,相当于从一个变量赋值到另一个变量。 原始值: 代码语言:javascript function add(num) { return num + 1; } let count = 5; let result = add(count); // 此处参数传递的过程可以看作是 num = count console.log(count); // 5 console.log(result); // 6 引用值: ...

October 2, 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

箭头函数

一、箭头函数的特点 1. 相比普通函数,箭头函数有更加简洁的语法。 普通函数 function add(num) { return num + 10 } 箭头函数 const add = num => num + 10; 2. 箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this。 这句话需要注意的是,箭头函数的外层如果有普通函数,那么箭头函数的this就是这个外层的普通函数的this,箭头函数的外层如果没有普通函数,那么箭头函数的this就是全局变量。 下面这个例子是箭头函数的外层有普通函数。 let obj = { fn:function(){ console.log('我是普通函数',this === obj) // true return ()=>{ console.log('我是箭头函数',this === obj) // true } } } console.log(obj.fn()()) 下面这个例子是箭头函数的外层没有普通函数。 let obj = { fn:()=>{ console.log(this === window); } } console.log(obj.fn()) // true 3. 箭头函数是匿名函数,不能作为构造函数,不可以使用new命令,否则后抛出错误。 ...

October 2, 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

解构赋值

解构赋值概念 解构赋值语法是一种 Javascript 表达式。 通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量。 解构赋值是通过将结构中的各元素复制到变量中来达到“解构”的目的。但对象/数组本身不会被修改。 数组解构 基本使用 📖 可以在定义变量的同时,使用解构进行赋值 let [one, two, three] = foo; //等同于 let one = foo[0]; let two = foo[1]; let three = foo[2]; 📖 也可以先声明变量,在进行解构赋值 设置默认值 当【左边≠右边】时,可以先设置一个默认值,否则取不到值的变量会变成undefined 剩余值处理 可以使用三个点 "..."+ 参数来接收剩余的所有元素,变量名称随意,必须在最后一个参数的位置上 进阶用法 🎨 可以和split 函数结合使用,比如 let [Mother,Father] = "judy,pete".split(','); console.log(Mother) //judy console.log(Father) //pete 🎨 可以使用逗号忽略元素,比如 let [name1,,name3] = ["Julius01", "Caesar02", "Consul03"] 🎨 可以直接赋值给对象的属性,比如 let user = {}; [user.name, user.surname] = ["Ilya","Kantor"] ...

October 2, 2024