1 关于JavaScript
JavaScript(JS)是单线程的、基于原型的、弱类型的、动态类型的、轻量的、支持面向对象/命令式/声明式编程的、头等函数的、多范式的、解释性(直译式或即时编译)的、也可在非浏览器环境下使用的动态脚本语言。JavaScript区分大小写。JavaScript 中,通过在运行时给空对象附加方法和属性来创建对象并且可以作为创建相似对象的原型。
浏览器 JavaScript 由三部分组成:核心 ECMAScript 描述了该语言的语法和基本对象;DOM 描述了处理网页内容的方法和接口;BOM 描述了与浏览器进行交互的方法和接口。
JavaScript 的动态特性包括运行时构造对象、可变参数列表、函数变量、动态脚本执行(通过 eval)、对象内枚举(通过 for ... in)和源码恢复(JavaScript 程序可以将函数反编译回源代码)。
JavaScript® 是 Oracle 在美国和其他国家的商标或注册商标。
2 编译原理与编译过程
在代码执行之前会经历词法分析、语法分析和代码生成的编译阶段。
词法分析(Lexing)会将由字符组成的字符串分解成有意义的代码块即词法单元(Token)。语法分析(Parsing)是将词法单元流转换成一个 由元素逐级嵌套所组成 的代表了程序语法结构的树即抽象语法树(AST,Abstract Syntax Tree)。代码生成是将 AST 转换为可执行代码。
V8 的执行过程:
源代码经过 Parser 解析器,经过词法分析和语法分析生成 AST;AST 经过 Ignition 解释器生成字节码并执行;在执行过程中,如果发现热点代码,将热点代码交给 TurboFan 编译器生成机器码并执行;如果热点代码不再满足要求,进行去优化处理即还原成字节码并把执行权交还给 Ignition 解释器。
3 执行上下文和作用域
JavaScript代码是运行在执行上下文中的,创建新执行上下文的方式有:
- 全局执行上下文:为函数之外的任何代码创建的上下文。
- 函数执行上下文:每个函数执行时创建的上下文。创建个数没有限制。
- eval执行上下文:eval函数执行创建的上下文,不建议使用可忽略。
每一个上下文在本质上都是一种作用域层级,每个上下文创建的时候会被推入执行上下文栈(Execution Context Stack,或者叫函数调用栈(Call Stack)),栈顶是当前执行的上下文,执行完代码退出的时候从上下文栈中移除,栈底是全局执行上下文。(注意:JavaScript 中,以回调方式调用的函数,是否会形成类似于递归那样“一层套一层” 的调用链而存在堆栈溢出的风险,取决于父函数执行完毕之前是否再次调用 “父” 函数,比如 setTimeout、nextTick、promise.then/catch/finally、MutationObserver等是在任务队列中执行,再次调用“父”函数时,父函数已经执行完毕,即不存在堆栈溢出的危险)。
变量对象(Variable object,VO)是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。作用域即执行上下文,由多个作用域的变量对象构成的链表就叫做作用域链。函数创建时,每个函数的[[Scopes]]属性在会保存所有父级作用域内的变量对象。
执行上下文中的代码会分成两个阶段进行处理
1. 进入执行上下文:
(1)如果是函数执行上下文,会复制函数[[scope]]属性到函数执行上下文的Scopes属性来初始化作用域链。
(2)然后创建变量对象:全局执行上下文的变量对象初始化是全局对象。函数执行上下文的变量对象初始化是只包含 Arguments 对象的对象。如果是函数执行上下文则首先给当前执行上下文的变量对象添加形参及初始值,否则先添加函数声明(函数表达式属于后面的变量声明)及初始值,再添加变量声明(带声明 var/let/const 关键字的)及初始值,完成后被激活为活动对象(Activation Object,AO);因此,变量提升(Hosting)是 JavaScript 执行上下文工作方式,意味着变量和函数的声明在编译阶段即执行阶段之前被放入内存中,就好像声明处在作用域最前面。由执行上下文的两个处理阶段可知,函数声明提升(非函数表达式)优先于变量声明提升(var、let、const);
(3)如果是函数执行上下文,会将活动对象压入函数执行上下文的Scopes属性即作用域链顶端。
2. 代码执行:
(1)访问并修改活动对象的属性值。对于let 或 const 声明的变量,从一个代码块的开始直到代码执行到声明变量的行之前,变量都处于“暂时性死区”(Temporal dead zone,TDZ),尝试访问即使是typeof也将抛出 ReferenceError;只有当代码执行到声明变量所在的行时,才会对其进行初始化(若未赋值,则初始化为 undefined)。
(2)执行完后将函数执行上下文从执行上下文栈中弹出。
作用域即执行上下文,由多个作用域的变量对象构成的链表就叫做作用域链。函数在执行前会复制该函数 [[Scopes]] 属性到函数执行上下文中创建作用域链。当访问一个变量时,解释器会首先在当前作用域的变量对象中查找标识符,如果没有找到,就去父作用域找,直到找到标识符,如果全局作用域也找不到就报错。因此,一个变量或函数在整个作用域链都没有声明,那么在代码执行阶段就会报ReferenceError错。
作用域共有两种主要的工作模式:词法作用域(静态作用域)和动态作用域。JavaScript采用词法作用域(Lexical Scope)。词法作用域根据源代码中声明变量的位置来确定该变量在何处可用,而动态作用域 并不关心函数和作用域是如何声明以及在何处声明,它只关心它们从何处调用。
const a = 2;
function foo() {
console.log(a);
}
function bar() {
const a = 3;
foo();
}
bar(); // 假如是动态作用域应该输出 3,而 JavaScript 是词法作用域,所以输出 2;
JavaScript的词法作用域分为:
1. 全局作用域:脚本模式运行所有代码的默认作用域。
2. 模块作用域: 模块模式中运行代码的作用域。 Node 中顶级作用域不是全局作用域,而是当前模块作用域。
3. 函数作用域:由函数创建的作用域。函数作用域指属于这个函数的全部变量都可以在整个函数的范围内访问。
使用 let(ES6)、const(ES6)声明的变量和 Function 构造函数声明的函数属于额外的块级作用域(被限制在定义它的代码块内——块由一对大括号界定)。
4 this
this是执行上下文的一个属性,在非严格模式下指向一个对象,严格模式下可以是任意值。
(1)在全局上下文(任何函数体外部)中,无论是否为严格模式,this指向全局对象。
(2)在函数上下文中,this的值取决于函数被调用的方式(运行时绑定,箭头函数除外),函数的调用方式包括:
默认情况即普通函数调用模式下,非严格模式中 this 指向 globalThis,严格模式中指向undefined,因此 this 可以用来判断当前是否处于严格模式:
function isStrict() {
return this === undefined;
}
(3)在类(class,ES6)上下文中,类中所有非静态的方法都会被添加到 this 的原型中。子类的构造函数中没有初始的this绑定,需要调用super()才会生成this绑定,因此在调用super()之前访问this会报错。而且子类不能在调用 super() 之前创建实例返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。类内部总是严格模式,调用一个 this 值为 undefined 的方法会抛出错误。
(4)在模块作用域中,this指向module.exports。
(5)当函数作为对象里的方法(包括getter、setter、原型链上的方法)、DOM事件处理函数、内联事件处理函数调用时,this 被设置为调用该函数的对象和元素。
(6)显示绑定函数的this方式:bind,call,apply,对箭头函数来说不能绑定 this即第一个参数会被忽略,只能传递参数。
(7)箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承 this,即箭头函数保持为创建时封闭词法作用域的this值(箭头函数不是封闭词法作用域)。关于“ this”的严格模式区别规则在箭头函数中将被忽略。针对箭头函数判定this的简便方法:在箭头函数创建的位置将整个箭头函数替换为this,则箭头函数的this就是这个被替换的this,如果有多层箭头函数,则重复这个步骤。
对象方法的链式调用通过该方法返回当前对象实现。
5 内存管理与垃圾回收
编程语言的内存生命周期均包括三部分:
- 分配所需要的内存,JavaScript 中发生在值的初始化和函数调用返回。
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还,即垃圾回收(Garbage collection,GC),是计算机编程中用于描述查找和删除那些不再被其他对象引用的对象处理过程。
JavaScript 的内存管理是自动的、无形的。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象,“对象”的概念包括 JavaScript 对象和函数作用域(或者全局作用域)。特别注意,全局变量的生命周期直到浏览器卸载页面才会结束,也就是全局变量不会被当成垃圾变量回收。
最初级的引用计数法:将“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”,因此如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。该方法存在循环引用的内存无法被回收的问题。
标记清除法:将“对象是否不再需要”简化定义为“对象是否可达”。假定设置一个设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象),如果一个值可以通过引用链从根访问,则认为该值是可达的。它相比引用计数法更好,因为“有零引用的对象”总是不可达,即使是循环引用。标记清楚算法缺陷是无法从根对象查询到的对象都将被清除。
栈内存,ESP指针下移(即上下文切换),栈顶的空间会自动被回收。
垃圾收集器进行分代收集(Generational collection)(按新生代和老生代收集)、增量收集(Incremental collection)(分多次收集)、闲时收集(在CPU空闲的时候)。
堆内存,分为新生代内存(临时分配的内存,存活时间短)和老生代内存(常驻内存,存活时间长)。
新生代的内存在 64 位和 32 位系统下默认限制分别为 32MB 和 16MB(存活时间短,操作更频繁,所以设置小)。
新生代内存空间分为 From(正在使用的内存)和 To (目前闲置的内存)。垃圾回收时使用Scavenge 算法:检查 From 内存,将存活对象从头放置的方式复制到 To 内存(按理说复制的同时,From 空间内存活对象的占用的内存应该释放掉),回收非存活对象,直到 From 内存空,From 内存空间和 To 内存空间角色对调。由于新生代存储的是生命周期短的对象,对象较少,因此时间性能优秀,算法缺点是新生代内存使用率不超过新生代内存的一半。为何不直接将非存活对象回收?一个对象的堆内存分配是连续的,直接回收非存活对象会容易产生不够后续对象使用的内存碎片,内存利用率低。
老生代内存,被晋升(1. 已经经历过一次 Scavenge 回收 2. 回收后To(闲置)空间的内存占用超过25%)的对象会被放入到老生代内存中。老生代垃圾回收(不使用Scavenge算法,老生代空间大,考虑内存利用率和复制耗时),采用标记清除算法:
第一步,标记-清除,遍历老生代中所有对象并做上标记,仅使用的变量以及被强引用的变量取消标记。标记结束后清除回收具有标记的变量对象。其中标记采用增量标记(为避免垃圾回收耗时阻塞时间过长,即将标记任务分为很多小的部分,小部分完成就暂停一下,直到标记阶段完成,如此,阻塞时间减少到原来的1/6)
第二步,整理内存碎片,清除阶段结束后把存活的对象全部往一端靠拢(移动对象,全过程最耗时操作)
堆栈溢出:指没有足够的内存空间申请。
内存泄漏指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存。当内存泄漏过多就会导致堆栈溢出。一般是堆内存溢出。
6 并发模型与事件循环
JavaScript 有一个基于事件循环(Event Loop)的并发模型,负责浏览器和Node中执行代码、收集和处理事件以及执行队列中的子任务,是用于解决JavaScript单线程运行时可能会阻塞的一种机制。
事件循环是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务的状态之间转换的无限循环。Javascirpt引擎大多数时候处于不执行任何操作即等待任务状态,仅在脚本加载完成/事件触发时执行。
渲染和执行JS是互斥的,都在渲染主线程中进行。执行JS时永远不会进行渲染(render)。仅在执行完成后才会绘制对 DOM 的更改。如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件或渲染。因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报来建议终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。
任务分为宏任务(MacroTask或Task)和微任务(MicroTack),每个宏任务之后,JavaScript引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。主动安排一个宏任务使用setTimeout/setInterval,主动安排一个微任务使用queueMicrotask。对于v11.15.0+ 的Node表现和浏览器一致,执行完一个定时器任务立即执行微任务队列中的所有任务,而之前版本的Node在执行完定时器任务后先检查任务队列的队首是否定时器任务,是则保存微任务队列,优先执行该定时器任务,中途产生的微任务进入微任务队列,待定时器任务执行完再依次执行微任务队列。
宏任务:script、setTimeout/setInterval(由计时线程计时,到期才放入队列中,且浏览器不会对同一个 setInterval 回调多次添加到任务队列,因此setInterval 的处理时长不能比设定的间隔长,否则 setInterval 将会没有间隔地重复执行)、setImmediate(Node.js)、I/O(Mouse Events、Keyboard Events、Network Events、文件读写)、UI Rendering(HTML Parsing)、MessageChannel等,多个宏任务形成宏任务队列。
微任务:promise.then/catch/finally 的回调、await下一行、process.nextTick(Node.js)、MutationObserver、垃圾回收过程、queueMicrotask的回调。其中调用 process.nextTick() 时传入的 callback 会被入队到nextTick callback queue。其余微任务的 callback 会被入队到microtask callback queue,且nextTick callback queue的优先级高于microtask callback queue。
浏览器事件(Event Loop)循环算法:
- 从宏任务队列中出队(dequeue)并执行最早的任务(例如script)。
- 依次执行当前微任务队列的所有任务。
- 如果有变更,则将变更渲染出来。
- 如果宏任务队列为空,则休眠直到出现宏任务。
- 转到步骤1。
每个阶段都会有一个 callback queue 与之相对应。Event Loop会遍历这个 callback queue,执行里面的每一个callback。直到 callback queue 为空或者当前callback的执行数量超过了某个阈值为止,Event Loop才会移步到下一个阶段。事件循环示意图:
- timer阶段:此阶段执行 timer callback queue中的任务,调用 setTimeout 或者 setInterval 方法时传入的 callback 会在指定的延迟时间后入队到 timer callback queue。调用时候传入的延迟时间并不是回调确切执行的时间,会受到操作系统调度层面和其他callback函数调用耗时的影响,timer callback函数的执行只会比预定的时间晚。
- pending callbacks:执行上一次循环延迟到下一次循环迭代的I/O回调,可能是系统操作的回调,如TCP错误的类型。
- idle、prepare阶段:只在node内部使用,不必理会。
- poll阶段:timer callback queue为空进入poll阶段,会计算应该为I/O进行阻塞和轮询的时长,然后处理I/O callback queue中的事件(几乎所有回调,比如文件读写、网络请求等,除了 close callback 、定时器调度的 callback 和 setImmediate)。若I/O callback queue非空则依次执行;若I/O callback queue为空且存在setImmdiate回调,直接进入check阶段,否则阻塞轮询到超时后进入自动check阶段。
- check阶段:I/O callback queue队列为空时,会立即进入该阶段,执行immediate callback queue。
- close callbacks阶段:比如socket被突然关闭(如socket.destroy()),"close "事件将在这个阶段被发出。否则,它将通过process.nextTick()发出。此阶段执行close callbacks queue 的任务。
阐述⼀下 JS 的事件循环?
事件循环,是浏览器渲染主线程的工作⽅式,是异步的实现方式。
在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息队列中取出第⼀个任务执⾏,⽽其他所有线程(包括其他进程的线程、计时线程)只需要在合适的时候将任务加⼊到队列末尾即可。任务本身没有优先级,在队列中先进行出,不同的队列有优先级。过去把任务队列简单分为宏队列和微队列,目前已无法满足复杂的浏览器环境,而是更加灵活多变的处理方式,根据 W3C 最新的解释:每个任务有不同的类型,同类型的任务必须在同⼀个队列,不同类型的任务可以属于同一队列。不同任务队列有不同的优先级,在⼀次事件循环中,由浏览器自⾏决定取哪⼀个队列的任务。但浏览器必须有⼀个微队列,微队列的任务⼀定具有最高的优先级,必须优先所有其他任务执⾏。在目前 chrome 的实现中,至少还包括优先级「中」的延时队列和优先级「⾼」的交互队列,分别用于存放计时器到达后的回调任务,和用于存放用户操作后产生的事件处理任务。
如何理解 JS 的异步?
JS 是⼀⻔单线程的语⾔,这是因为它运⾏在浏览器的渲染主线程中,⽽渲染主线程只有⼀个。⽽渲染主线程承担着诸多的⼯作,渲染⻚⾯、执⾏ JS 都在其中运⾏。如果使用同步的⽅式,就极有可能导致主线程产⽣阻塞,从⽽导致消息队列中的很多其他任务⽆法得到执⾏。这样⼀来,⼀⽅⾯会导致繁忙的主线程⽩⽩的消耗时间,另⼀⽅⾯导致⻚⾯⽆法及时更新,给⽤户造成卡死现象。所以浏览器采⽤异步的⽅式来避免。具体做法是当某些任务发⽣时,⽐如计时器、⽹络、事件监听,主线程将任务交给其他线程去处理,⾃身⽴即结束任务的执⾏,转⽽执⾏后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加⼊到消息队列的末尾排队,等待主线程调度执⾏。在这种异步模式下,浏览器永不阻塞,从⽽最⼤限度的保证了单线程的流畅运⾏。单线程是异步产生的原因。
JS 中的计时器能做到精确计时吗?
不⾏,因为:
- 计算机硬件没有原子钟,⽆法做到精确计时;
- 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差;
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级 >= 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时⼜带来了偏差;
- 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此⼜带来了偏差。
7 函数
在 JavaScript 中,函数是头等对象,因为它们可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于函数可以被调用。每个 JavaScript 函数实际上都是一个 Function 对象(运行 (function(){}).constructor === Function 为true),函数调用的默认返回值为undefined。
7.1 函数创建
创建函数的方式包括函数声明、函数表达式(包括立即执行函数表达式)、箭头函数表达式(参考7.9 箭头函数)、Function构造函数(参考 7.8 闭包首图)、函数生成器表达式、函数生成器声明、GeneratorFunction构造函数(与Function构造函数类似,字符串作为函数体会阻止一些JS引擎优化和存在安全问题)。
函数声明:
函数表达式:
函数生成器声明(生成器实现机制(即如何让函数暂停和恢复)是协程,一个线程可以存在多个协程(函数可以看做一个协程),协程被程序自定义所控制,而不受操作系统的管理,并不会像线程切换那样消耗资源。单个线程同一时刻只能一个协程(即获得线程控制权)运行):
函数生成器表达式声明:
7.2 函数参数
调用函数时,传递给函数的值被称为函数的实参(值传递,即“原始值”传递的是值的拷贝,“引用值”传递的是指向引用对象的地址),对应位置的函数参数名叫作形参。函数的参数默认是undefined。严格模式下不允许出现同名参数。
默认参数(ES6)允许在没有值或undefined被传入时使用默认形参。前面的参数可用于后面的默认参数被访问,但前面的默认参数表达式不能访问后面的(包括函数体内),可以叫做默认参数的暂时性死区(TDZ)。默认参数的位置不存在限制,但建议放在所有非默认参数后面。默认参数支持解构赋值(参考解构赋值)。
剩余参数(ES6)允许将一个不定数量的参数表示为一个真数组。
JavaScript 函数不能像传统意义上那样实现重载。而在其他语言中,可以为一个函数编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。
JavaScript 函数没有签名,因为其参数是由包含零个或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。只能通过检查传入函数中参数的类型和数量(实现方式有arguments或剩余参数)并作出不同的反应,来模仿方法的重载。
7.3 arguments
arguments对象是所有(非箭头)函数中都可用的局部变量, 是一个对应于传递给函数的参数即实参的类数组对象,实现有Symbol.iterator方法,是一个可迭代对象。“类数组”意味着 arguments 有length属性并且属性的索引是从零开始的,但是它没有 Array的内置方法。将arguments转化为真数组的方式:
调用函数的实参个数为零时,形参的值与arguments对象的值互不影响。在严格模式下,无论剩余参数、默认参数和解构赋值参数是否存在,arguments对象和参数的值均互相不影响。而非严格模式中,函数没有(有)包含剩余参数、默认参数和解构赋值,那么arguments对象中的值和参数的值互相(不)影响。
arguments.callee属性表示当前正在执行的函数。 在严格模式下,第 5 版 ES5禁止使用 arguments.callee()。当一个函数必须调用自身的时候,避免使用 arguments.callee(),通过要么给函数表达式一个名字,要么使用一个函数声明。
arguments.callee.caller 返回调用指定函数所处的函数。无论是否作为对象方法调用,如果一个函数 f 是在全局作用域内被调用的,则为 null。相反,如果一个函数是在另外一个函数作用域内被调用的,则指向调用它的那个函数。
arguments.length 本次函数调用时传入函数的实参数量。
7.4 Function原型属性与方法
Function([arg0, ... , argN, ]functionBody) 构造函数创建了一个新的 Function 对象,Function构造函数的隐式原型(__proto__)指向Function.prototype。直接调用构造函数可以动态创建函数,但可能会经受一些安全和类似于 eval()(但远不重要)的性能问题。使用 Function 构造函数创建的 Function 对象会在函数创建时完成解析。这比用函数表达式或函数声明创建一个函数并在代码中调用它的效率要低,因为使用表达式或声明创建的函数会和其他的代码一起被解析。然而,不像 eval(可能访问到本地作用域),Function 构造函数只创建全局执行的函数。调用 Function() 时可以使用或不使用 new。两者都会创建一个新的 Function 实例:
- argN:被函数用作形参的名称。每一个必须是字符串,对应于一个有效的 JavaScript 参数(任何一个普通的标识符、剩余参数或解构参数,可选择使用默认参数),或用逗号分隔的此类字符串的列表。由于参数的解析方式与函数表达式的解析方式相同,所以接受空白和注释。
- functionBody:一个包含构成函数定义的 JavaScript 语句的字符串。
Function.prototype.displayName 属性获取函数的显示名称,默认是没有设置的,可以赋值,但该特性是非标准的,请尽量不要在生产环境中使用它。
Function.prototype.prototype 属性是使用new运算符调用构造函数时,构造函数的 prototype 属性将成为新对象的原型。默认情况下,构造函数的 prototype 是一个普通的对象。这个对象具有一个属性:constructor,它是对构造函数本身的一个引用,constructor 属性是可编辑、可配置但不可枚举的。如果prototype 被赋予了 Object 以外的值,则当它被 new 运算符调用时,返回对象的原型将会指向Object.prototype(换句话说,new 运算符会忽略它的prototype属性并构造一个普通对象)。Function.prototype.bind返回的绑定函数不具有 prototype 属性,但是可以作为构造函数。而异步函数,箭头函数没有prototype属性,不能成为构造函数,即便手动添加prototype属性。生成器函数、Symbol、BigInt有prototype 属性,但它不能作为构造函数。
Function.prototype.apply调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。apply可以用来针对只接受参数列表的函数调用时避免循环,或者直接使用展开语法(...)。
Function.prototype.call方法使用一个指定的 this 值和单独给出的参数列表来调用一个函数。语法与apply几乎相同,但根本区别在于,call接受一个参数列表,而 apply 接受一个为数组或类数组对象的参数。
Function.prototype.bind方法创建一个新的函数,并拥有指定的 this 值和初始实参,即该函数在调用时,会将 this 设置为bind提供的 thisArg,而新参数会接续在bind中传递的参数列表之后。如果使用 new 运算符构造该函数,则会忽略thisArg。
Function.prototype.toString方法返回一个表示当前函数源代码的字符串,而且得到的源代码时准确的,注释、空格也是包括在内的,该方法覆盖 Object.prototype.toString方法。在 Function 需要表示为字符串时,JavaScript 会自动调用函数的 toString 方法(比如函数与一个字符串进行拼接或包装在模板字符串中)。若调用的this不是Function对象,则 toString() 方法将抛出 TypeError。如果是在内置函数或由 Function.prototype.bind 返回的函数上调用 toString(),则toString() 返回原生代码字符串("function someName() { [native code] }"),其中someName是实现定义的名称或函数的初始名称。对原生函数的字符串调用 eval() 将始终产生语法错误。若是在由 Function 构造函数生成的函数上调用 toString(),则 toString() 返回创建后的函数源码,包括形参和函数体,函数名为“anonymous”。
实现apply:
实现call:
实现bind:
7.5 getter & setter
get 语法将对象属性绑定到查询该属性时将被调用的函数。set 语法将对象属性绑定到要设置属性时将被调用的函数。getter 和 setter 通常用于创建和操作一个伪属性。可以使用 delete 操作符移除 getter 和 setter 创建的伪属性。getter 或setter可以用 Object.defineProperty 添加到现有对象上。在 Classes 中使用时,get 和 set 关键字语法是定义在原型上的,Object.defineProperty 是定义在实例自身上。
getter 设置的伪属性在访问它们之前不会计算属性的值,能延迟计算值的成本。当属性值的计算是昂贵的(占用大量 RAM 或 CPU 时间,产生工作线程,检索远程文件等)、或者现在不需要该值而是在稍后甚至某些情况根本不使用、或者多次访问值不改变也就不需要被重新计算时,可用智能(或称记忆化)getters 延迟属性值的计算并将其缓存返回缓存值以备以后访问而不需要重新计算。示例:
7.6 异步函数(async)
异步(async)函数是在普通函数前添加async关键字声明的函数,它是 AsyncFunction 构造函数的实例,并且在且仅在其中允许使用 await 关键字,async/await 的行为就好像搭配使用了生成器和 promise,避开链式调用Promise。使用 async/await 关键字,可通过同步的方式书写异步代码。
async 函数可能包含 0 个或者多个 await 表达式,从第一行代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行的,因此一个不含 await 表达式的 async 函数是会同步运行的,然而,如果函数体内有一个 await 表达式,async 函数就一定会异步执行。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其await的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的resolve值会被当作该 await 表达式的返回值。
在await 表达式之后的代码可以被认为是存在在链式调用的 then 回调中,多个 await 表达式都将加入链式调用的 then 回调中,返回值将作为最后一个 then 回调的返回值。任何一个 await 语句后面的 Promise 对象变为 rejected 状态,如果该promise没加catch捕获,或使用try/catch捕获,那么整个 async 函数都会中断执行,并通过隐式返回 Promise 将错误传递给调用者。
优雅的try/catch处理 async /await错误:
sync函数返回一个async 函数成功返回的值被 resolve 或 async 函数中抛出的(或其中没有被捕获到的)异常被reject的Promise,即使async函数return看起来不是Promise,也会隐式的被Promise.resolve包装。
return await Promise和 return Promise 区别在于前者返回的是解决后的返回值的Promise,后者返回的是该Promise是异步的,是不能使用try/catch捕获的,await Promise 才可以。
async/await重试逻辑实现:
async/await 实现原理:
async/await应用场景:
7.7 new操作符
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new可以看做下列操作的语法糖:
- 创建一个新对象,使用构造函数的原型来作为新创建对象的原型(prototype)
- 将新对象作为this调用构造函数(apply)
- 如果构造函数没有返回对象(即返回的是非引用类型),则返回this,否则返回构造函数返回的对象。
对于普通对象,无论性能上还是可读性,更推荐使用字面量的方式创建对象。(否则new涉及到可能通过原型链一层层查找到Object)。
实现new:
new.target 属性允许你检测函数或构造方法是否是通过new运算符被调用的。在通过new运算符被初始化的函数或构造方法中,new.target返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是undefined。
new的优先级?
new相关的部分优先级从高(20)变低:
带参数列表的new即new Foo()的优先级、函数调用以及成员访问同级,且大于无参数列表的new的优先级new Foo,虽然new Foo()等同于new Foo。
7.8 闭包
闭包(closure)是将一个函数与对其周围状态(词法环境)的引用捆绑在一起(封闭)的组合。闭包产生的本质是当前函数存在对父级作用域的引用,因此JavaScript 中的所有函数都是闭包的(new Function例外)。
被引用的变量即自由变量(当前函数作用域未声明而访问的变量,不包括函数参数arguements)。闭包也可以捕获块作用域和模块作用域中的变量。
闭包应用场景:
循环中的闭包问题?
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次。
理论上当函数可达时,它外部的所有变量也都将存在。但在实际中,JavaScript 引擎会试图优化它:分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
7.9 箭头函数(ES6)
引入箭头函数有两个方面的作用:更简短的函数并且运行时不绑定this。
箭头函数保持为创建时封闭词法作用域的this或arguments值(箭头函数不是封闭词法作用域)。箭头函数内没有自己的this(所以从上层作用域去找), arguments(所以从上层作用域去找),super,new.target,prototype。箭头函数不能用作构造函数(会报TypeError错)。yield关键字不能在箭头函数中使用即不能作为 Generator 函数。箭头函数在参数和箭头之间不能换行。
7.10 构造函数
生成器函数,Math,JSON,Symbol,Reflect,Atomics,BigInt 不能作为构造函数,也就不能使用 new 运算符。
7.11 惰性函数
惰性函数表示函数执行的分支只会在函数第一次调用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。常见的检测浏览器支持情况选择为 DOM 节点添加事件监听的函数:
7.12 级联函数
级联函数也叫链式函数,是一种在一个对象上使用一条连续的代码来重复调用不同方法的技巧。一定程度上可以减少代码量,提高代码可读性,缺点是它占用了函数的返回值。比如字符串方法,jQuery方法。要使用级联函数,我们只需要在每个函数中返回 this 对象(也就是后面方法中操作的对象)。操作的对象就会在执行完一个函数后继续调用往后的方法,即实现了链式操作。
7.13 高阶函数
高阶函数指操作函数的函数,一般地,有以下两种情况:
- 函数作为参数被传递:把函数当作参数传递,代表可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。比如回调函数,包括Ajax,事件监听,数组排序方法sort等
- 函数作为返回值输出。比如偏函数(Partial),返回了一个包含预处理参数的新函数,以便后续逻辑可以调用。在计算机科学中,Partial应是指将部分参数固定,从而产生一个新的较小元(元即参数的个数)的函数。偏函数是把一个 n 元函数转换成一个 n - x 元函数,比如Function.prototype.bind。
AOP 即面向切面编程,它的主要作用是 把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过动态植入的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。通常,在 JavaScript 中实现 AOP,都是指把一个函数动态植入到另外一个函数之中。
7.14 函数柯里化与函数反柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。柯里化是利用闭包的特性实现的。完全柯里化指的是将函数变换成每次只接受一个参数的新函数,直到参数个数等于原函数即返回结果,即柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)。而 JavaScript 中大多数的柯里化实现都是高级版的,即使得函数被多参数变体调用。
柯里化的优点:1. 参数复用(返回的函数可复用前面的参数); 2. 延迟执行(返回函数);3. 提前返回(提前做判断,返回指定需求的函数)。
柯里化的缺点:1. 闭包和嵌套作用域带来的性能损耗;2. apply与call等显然比直接调用慢。
柯里化的实现:
以上的柯里化实现要求原函数具有固定数量的形参,如果是使用剩余参数的函数,例如 f(...args),不能以这种方式进行柯里化。
柯里化是为了缩小适用范围,创建一个针对性更强的函数;反柯里化则是扩大适用范围,创建一个应用范围更广的函数。
7.15 函数记忆与函数睡眠
函数记忆 指将上次的(计算结果)缓存起来,当下次调用时,如果遇到相同的(参数),就直接返回(缓存中的数据)。实现原理是将参数和对应的结果保存在对象中,再次调用时,判断对象 key 是否存在,存在返回缓存的值。
函数睡眠,在JavaScript中是一个伪命题,因为JavaScript 引擎线程无法挂起,只能通过异步实现类似 sleep 的效果。
7.16 函数防抖
防抖(debounce),即短时间内大量触发同一事件,只会执行一次函数,实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作(类似于生活中的电梯关门),防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费。
使用函数防抖的三个条件:
- 频繁调用某个函数;
- 造成效率问题;
- 需要的结果以最后一次调用为准。
7.17 函数节流
节流(throttle)是间隔执行,规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。实现原理为设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器,和防抖的区别在于,防抖每次触发事件即重置定时器,而节流在定时器到时间后再清空定时器。
7.18 函数管道与组合
函数组合是一种将简单函数组合起来构建更复杂函数的行为或机制,函数组合对传入的多个简单函数从右到左执行,函数管道则刚好相反。
8 浏览器事件相关
在 Web 中,事件在浏览器窗口中被触发并且通常被绑定到窗口内部的特定部分 — 可能是一个元素、一系列元素、document 或者是 window。每个可用的事件可设置事件处理器(或事件监听器),监听事件的发生,也就是事件触发时会运行的代码块。
实际上,JavaScript 网页上的事件机制不同于在其他环境中的事件机制。Nodejs 中事件模型(nodejs event model)依赖定期监听事件的监听器和定期处理事件的处理器,比如使用 on 方法注册一个事件监听器,使用once方法注册一个在运行一次之后注销的监听器。浏览器插件(WebExtensions)的事件模型和网站的事件模型相似,唯一不同在于事件监听属性是小驼峰的且需要紧接着 addListener。(比如browser.runtime.onMessage.addListener)。网络事件不是 JavaScript 语言的核心——它们被定义成内置于浏览器的 JavaScript APIs。
事件分为通用事件(比如几乎所有元素的onclick)和专门事件(video才有的onplay)。每个事件都使用继承自 Event 接口的对象来表示,可以包括额外的自定义成员属性及函数,以获取事件发生时相关的更多信息。Event 接口表示在 DOM 中出现的事件。触发方式有
- 用户触发,比如鼠标、键盘事件。
- API生成,比如动画运行结束。
- 脚本触发,比如对元素调用HTMLElement.click或使用Even接口定义一些自定义事件,再使用 EventTarget.dispatchEvent方法将自定义事件派发往指定的目标(EventTarget)或直接使用EventTarget.dispatchEvent触发原生事件。和经由浏览器触发,并通过事件循环异步调用事件处理程序的“原生”事件不同,dispatchEvent() 会同步调用事件处理函数。在 dispatchEvent() 返回之前,所有监听该事件的事件处理程序将在代码继续前执行并返回。要向事件对象添加更多数据,可以使用 CustomEvent 接口,detail 属性可用于传递自定义数据。
事件处理函数内部,事件对象被自动以第一个参数传递给事件处理函数,以提供额外的功能和信息。大多数事件处理器的事件对象都有可用的标准属性和函数(方法),更高级的事件处理器的事件对象会添加一些专业属性,这些属性包含它们需要运行的额外数据。例如,媒体记录器 API 有一个dataavailable事件,它会在录制一些音频或视频时触发,并且可以用来保存它,或者回放。对应的ondataavailable处理程序的事件对象有一个可用的数据属性。
在事件处理程序内部,对象 this 始终等于 Event.currentTarget 的值且等于注册事件的对象,Event.target 则是对最初派发事件的目标的引用。
只读属性Event.isTrusted 表示当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent() 派发的时候,这个属性的值为 false 。
Event() 构造函数:
使用事件处理程序 HTML 属性(内联事件处理程序),会混用 HTML 和 JavaScript,而且没有直接移除事件的方式,不推荐使用,因为这样文档很难解析且不好维护。
DOM0级事件通过事件处理程序属性添加事件监听器,不允许给同一个监听器注册多个处理器,任何后面设置的都会尝试覆盖之前的,但具有更好的跨浏览器兼容性(IE8+)。
DOM 2级事件可以使用addEventListener添加事件处理程序,而且如果有需要,可以向同一类型的元素添加多个事件处理器,同时可以使用removeEventListener()移除某个事件处理程序,但需要保证 type, listener,capture/useCapture和addEventListener的相同。
过去Netscape(网景)只使用事件捕获,而 Internet Explorer 只使用事件冒泡。DOM 2级事件规定的事件流(也叫事件传播)包括三个阶段,Event.eventPhase表示事件流当前处于哪一个阶段:
- 事件捕获阶段(Capture Phase):事件对象,从 Window 开始,然后 Document, 然后是 HTMLHtmlElement,直到目标元素的父元素,这个过程中通过 addEventListener 注册为捕获模式(第三个参数为 true)的事件处理程序会被调用。Event.eventPhase 为 1。
- 处于目标阶段(Target Phase):执行顺序会按照 addEventListener 的添加顺序决定,现添加先执行。Event.eventPhase 为 2。如果该事件只读属性的 Event.bubbles为false,则不会进入冒泡阶段。如果多个事件监听器被附加到相同元素的相同事件类型上,会按其被添加的顺序被调用,如果如果在其中一个事件处理函数中执行 Event. stopImmediatePropagation,那么剩下的事件监听器都不会被调用。
- 事件冒泡阶段(Bubbling Phase):事件对象从父亲元素开始逆向向上传播回目标元素的祖先元素并最终到达包含元素 Window。这个过程中通过 addEventListener 注册为冒泡模式(第三个参数为false)的事件处理程序会被调用。Event.eventPhase 为 3。可以通过在事件处理程序return之前设置 Event.cancelBubble 为 true 或调用 Event.stopPropagation 方法阻止事件冒泡。
调用方法 Event.preventDefault 或设置属性 Event.returnValue 为 true 来阻止事件的默认行为,比如阻止表单默认提交行为。为一个不支持取消即只读属性的 Event.bubbles 为 true 的事件调用preventDefault 将没有效果。
事件委托依赖于事件冒泡,如果想要在大量子元素中单击任何一个都可以运行一段代码,可以将事件监听器设置在其父节点上,并让子节点上发生的事件冒泡到父节点上,而不是每个子节点单独设置事件监听器。
9 数据类型相关
数据类型分为基本数据类型(原始值)和引用数据类型。
9.1 基本数据类型
基本类型:存储在栈中的简单数据段。值直接存储在变量访问的位置(栈空间),因为原始值占据的空间是固定的,所以可存储在较小的内存区域 – 栈中,便于迅速查寻变量的值。变量赋值时,将原始值的副本赋值给新变量,两个变量完全独立,只是拥有相同的 value。
JavaScript中存在7种基本数据类型(原始值):boolean、bigInt(ES10)、null、undefined、number、string、symbol ( ES6——表示独一无二的值),其中undefined、 null 和 number字面量是没有属性和方法的(直接调用方法1.toString()会失败,通过let a = 1;a.toString()却可以成功),或者字符串字面量可以直接调用方法(隐式创建包装器对象)。
9.2 引用数据类型
引用类型:存储在堆(heap)中。存储在变量处的值是一个指针(point),指向存储堆中对象的内存地址。因为引用值(堆中对象)的大小会改变,放在栈中会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆空间中对应的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。变量赋值时,把存储在栈空间的内存地址赋值给新变量,即两个变量都指向堆内存中的同一个对象,任何一个变量的属性作出的改变都会反映在另一个身上。复制不会产生新的堆内存消耗。
除基本类型外都是对象类型(Object)—引用类型,包括Object(父对象)、Array(数组对象)、RegExp(正则对象)、Date(日期对象)、Math(数学函数)、Function(函数对象)、Error(错误对象)、Arguments(类数组对象)、自定义对象等
9.3 类型检测与转换
类型检测的方法有,typeof、instanceof、Object.prototype.toString、constructor。
typeof 运算符返回一个字符串,表示操作数的类型。基本数据类型除null返回object,都正确,引用类型除函数对象返回Function,其余均返回Object。因为在 JavaScript 最初的实现中,在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的类型标签(1-3 bits) 以及当前要存储值的真实数据。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。对于声明未初始化和未声明的变量,typeof都会返回undefined,但不能在let或const的暂存性死区内使用,会抛出ReferenceError。适合检测非null的基本数据类型和引用数据类型中的function。typeof 运算符的优先级
高于加法(+)等二进制运算符。
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。instanceof的检测结果不一定正确,构造函数的 prototype 属性对象可能被修改,实例对象的__proto__也可能被修改。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数,比如 [] instanceof window.frames[0].Array 会返回false。Symbol.hasInstance用于自定义判断某对象是否为某构造器的实例时应该调用的方法,即使用 instanceof时会调用到该方法。
Object.prototype.toString.call(value)返回"[object Type]",Type 是value的类型。如果value有 Symbol.toStringTag 属性,其值是一个字符串,则它的值将被用作 Type。可以通过定义 Symbol.toStringTag 属性来更改 Object.prototype.toString.call() 的行为,因此也会被篡改。
value.constructor属性获取value的构造函数的原型对象上的constructor属性,该属性实际上是原型对象的constructor属性,也因此如果原型对象被修改,就不能用来判断类型了。
实现获取一个值的类型的函数:
JavaScript中存在的类型转换规则:
- 转换为布尔值(ToBoolean):除falsy值(false、-0、+0、0n、null、undefined、NaN、""、 ''、 ``)转换为 false,其余均转换为true。
- 转换为数字(To Number):
- 基本数据类型:
- null 转换为 0;
- 空字符串,以及空格、制表符、换行符的各种组合,转化为0,有效的纯数字转换为数字,其余按失败处理转化为NaN;
- undefined 转换为 NaN
- true 转换为 1,false转换为 0;
- 引用数据类型:先转换为基本数据类型(ToPrimitive),再按基本数据类型处理。
- 基本数据类型:
- 转换为字符串:
- 基本数据类型:
- null 转换为 'null';
- undefined转为 "undefined";
- true 转为 "true",false 转为 "false";
- Number类型转为数字的字符串形式,极小和极大的数字会使用指数形式;
- 引用数据类型:先转换为基本数据类型(ToPrimitive),再按基本数据类型处理。
- 基本数据类型:
- 转换为基本数据类型(ToPrimitive):
- 首先如果[@@toPrimitive](hint)即Symbol.toPrimitive存在,则调用该方法,该方法必须返回原始值——返回对象会导致 TypeError。
- 如果该方法不存在,如果hint为"number",按先调用valueOf() 再 toString()的顺序。如果hint为"string",按先调用toString()再valueOf()的顺序。如果前一个返回对象,则忽略其返回值,从而使用后一个的返回值。如果两个方法都不存在或都不返回原始值,则抛出TypeError。注意:Date.prototype[@@toPrimitive] 的hint视为"string"调用 toString() 。Symbol.prototype[@@toPrimitive] 忽略 hint,并总是返回一个 symbol。
a == 1 && a == 2 如何返回true?
四则运算中转换规则:
- 非加法,将两个操作数均转化为数字,再计算。
- 加法, a+b 为例,先计算a,再计算b,然后相加。
- 如果a 或 b 中有对象,将对象转为原始值,hint为"number",存在[@@toPrimitive](hint)则先调用,否则再valueOf,再toString(),
- 均转为原始值后,如果其中一个是字符串,另一边转换为字符串进行拼接,否则,两边都转换为Number进行自然加法运算。
比较运算符类型转换:1. 如果是对象,就通过 toPrimitive 转换对象 2. 如果是字符串,就通过 unicode 字符索引来比较。
9.4 判断相等
== (抽象相等运算符),进行必要类型转换再比较。类型转换规则如下:
- 类型相同,对象看是否为同一引用,基本数据类型看值是否相同。
- 类型不同:
- 其中一个为对象,另一个是基本数据类型,将对象转换为基本类型
- 两个均为基本数据类型:
- 类型相同,看值是否相同。
- 其中一个是null,另一个是undefiend,返回true。
- 如果其中一个操作数是 Symbol 而另一个不是,返回 false。
- 如果其中一个是布尔值而另一个不是,将布尔值转换为数字。
- 其中一个是number,另一个是string,将string转为number。
- 其中一个是number,另一个是BigInt,按数值进行比较。如果其中一个数值为 ±∞ 或 NaN,返回 false。
- 其中一个是string,另一个是BigInt,将string转为number。
=== (严格相等运算符)判定规则:
- 如果操作数的类型不同,则返回 false。
- 如果两个操作数都是对象,只有当它们指向同一个对象时才返回 true。
- 如果两个操作数都为 null,或者两个操作数都为 undefined,返回 true。
- 如果两个操作数有任意一个为 NaN,返回 false。
- 否则,比较两个操作数的值,数字类型必须拥有相同的数值。+0 和 -0 会被认为是相同的值。字符串类型必须拥有相同顺序的相同字符。布尔运算符必须同时为 true 或同时为 false。由于不进行类型转换因此会更快。
实现Object.is(ES6,在严格相等的基础上处理包括 +0 和 -0 应该不相等,NaN 和 NaN 应该相等的特殊情况):
9.5 Number
JavaScript 的 Number 对象是经过封装的能处理数字值的对象。Number 对象由 Number() 构造器(继承自 Function)创建。所有 Number 实例都继承自 Number.prototype。在非构造器上下文中 (如:没有 new 操作符),Number 能被用来执行类型转换。如果参数无法被转换为数字,则返回 NaN。
十进制数值字面量能以 0 开头,但是如果 0 以后的最高位比 8 小,数值将会被认为是八进制而不会报语法错误。对于位数过多的数值字面量建议使用科学计数法nem或nEm,即n * 10m。
JavaScript 的 Number 类型为双精度 IEEE 754 64 位浮点类型(符号+指数+尾数)。JavaScript 能够准确表示的整数范围在[-(2^53 - 1),2^53 - 1]。在解析序列化的 JSON 时,如果 JSON 解析器将它们强制转换为 Number 类型,那么超出此范围的整数值可能会被破坏。在工作中使用String 类型代替,是一个可行的解决方案。
number原始值可以通过小括号、’+’/’-’、或者Number( ) 转换成 Number对象。
Number构造器的属性(其属性特性writable(可重写)、enumerable(可枚举)、configurable(可配置)均为false)与方法:
(1)Number.EPSILON 属性表示 1 与Number可表示的大于 1 的最小的浮点数之间的差值,值接近于 2.2204460492503130808472633361816E-16,或者 2^-52。
(2)Number.MAX_SAFE_INTEGER(Number.MIN_SAFE_INTEGER ) 常量表示在 JavaScript 中最大(小)的安全整数2^53 - 1 = 9007199254740991 (-(2^53 - 1) = -9007199254740991),安全存储是指能够准确区分两个不相同的值(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 为 true)。
(3)Number.MAX_VALUE(Number.MIN_VALUE) 属性表示在 JavaScript 里所能表示的最大(小正)值,约为 1.79E+308(5e-324)。大(小)于 MAX_VALUE(MIN_VALUE ) 的值代表 "Infinity"(0)。超过最大正数或最小负数的值会被转化为Infinity或者-Infinity而无法继续参与运算。
(4)Number.NEGATIVE_INFINITY (Number.POSITIVE_INFINITY)属性表示负(正)无穷大。Number.NEGATIVE_INFINITY 的值和全局对象的 Infinity 属性的负(正)值相同。 使用 isFinite 方法比 Number.NEGATIVE_INFINITY 属性来判断值是否有限更好。与数学上的无穷大存在区别:
(5)Number.NaN 表示“非数字”(Not-A-Number),和全局属性 NaN 相同,属于falsy值,且NaN有方法。NaN 及其行为不是 JavaScript 发明的。它在浮点运算中的语义(包括 NaN !== NaN)是由 IEEE 754 指定的。NaN 的行为包括:
1. 如果 NaN 涉及数学运算(但不涉及位运算),结果通常也是 NaN。
2. 当 NaN 是任何关系比较(>, <, >=, <=)的操作数之一时,结果总是 false。
3. NaN 不等于(通过 ==、!=、=== 和 !==)任何其他值——包括与另一个 NaN 值。
有五种不同类型的操作返回 NaN:
- 失败的数字转换。
- 计算结果不是实数的数学运算。
- 不定式(0 * Infinity、1 ** Infinity、Infinity / Infinity、Infinity - Infinity)。
- 一个操作数被强制转换为 NaN 的方法或表达式,NaN具有传染性。
- 0 除以 0(正数除以0返回 Infinity,负数除以0返回- Infinity)。
- 将无效值表示为数字的其他情况。
判断一个值value是否为 NaN,可以使用isNaN(value) 或Number.isNaN(value) 或value !== value。 isNaN() 和 Number.isNaN() (ES6)之间的区别:isNaN会将非Number类型转换后再判断,后者更可靠,ES6之前使用value !== value更可靠。使用 BigInt 值时 isNaN() 会抛出TypeError错误,而 Number.isNaN() 不会。查找索引的数组方法(indexOf()、lastIndexOf())不能找到 NaN,而查找值的(includes())可以。
在 IEEE 754 编码中,任何指数位为 0x7ff 且尾数位非零的浮点数都是 NaN。在 JavaScript 中,可以使用类型化数组来进行位操作。
NaN 被静默转义(相对于被忽略)的唯一情况是使用指数为 0 求幂时,它立即返回 1 而不测试基数的值。
(6)Number.isNaN() 方法确定传递的值是否为 NaN,并且检查其类型是否为 Number。
(7)Number.isInteger() 方法用来判断给定的参数是否为整数。NaN 和正负 Infinity 不是整数。
(8)Number.isFinite() 方法用来检测传入的参数是否是一个有穷数。和全局的 isFinite() 函数相比,这个方法不会强制将一个非数值的参数转换成数值,这就意味着,只有数值类型的值,且是有穷的(finite),才返回 true。
(9)Number.isSafeInteger() 方法用来判断传入的参数值是否是一个“安全整数”(safe integer)。安全整数范围为 [-(2^53 - 1) , 2^53 - 1 ],3.0和3均是安全整数。一个安全整数是一个符合下面条件的整数:
- 可以准确地表示为一个 IEEE-754 双精度数字,
- 其 IEEE-754 表示不能是舍入任何其他整数以适应 IEEE-754 表示的结果。
比如,2^53 - 1 是一个安全整数,它能被精确表示,在任何 IEEE-754 舍入模式(rounding mode)下,没有其他整数舍入结果为该整数。作为对比,2^53 就不是一个安全整数,它能够使用 IEEE-754 表示,但是 2^53 + 1 不能使用 IEEE-754 直接表示,在就近舍入(round-to-nearest)和向零舍入中,会被舍入为 2^53。
(10)Number.parseFloat() 方法(ES6)可以把一个字符串解析成浮点数,若无法被解析成浮点数,则返回NaN。该方法与全局的 parseFloat() 函数相同。
- 如果 parseFloat 在解析过程中遇到了正号(+)、负号(- U+002D HYPHEN-MINUS)、数字(0-9)、小数点(.)、或者科学记数法中的指数(e 或 E)以外的字符,则它会忽略该字符以及之后的所有字符,返回当前已经解析到的浮点数。
- 第二个小数点的出现也会使解析停止;
- 参数首位和末位的空白符会被忽略。
- 如果参数字符串的第一个字符不能被解析成为数字,则 parseFloat 返回 NaN;
- parseFloat 也可以解析并返回 Infinity。
- parseFloat 解析 BigInt 为 Numbers, 丢失精度。因为末位 n 字符被丢弃。
- parseFloat 也可以转换一个已经定义了 toString 或者 valueOf 方法的对象,它返回的值和对 toString 或者 valueOf 方法的返回结果上调用 parseFloat 值相同。
- 考虑使用 Number(value) 进行更严谨的解析:只要参数带有无效字符就会被转换为 NaN。
(11)Number.parseInt(string[, radix]) 方法依据指定基数,解析字符串并返回一个整数。如果参数不是一个字符串,则将其强制转化为字符串。字符串开头的空白符将会被忽略,若第一个非空白字符不能转换为数字,则返回 NaN。radix是从 2 到 36 的整数,表示进制的基数,如果超出这个范围,将返回 NaN。假如 radix 未指定或者为 0,若数字以 0x 或 0X 开头,则radix为 16,如果输入的 string 以 "0"(0)开头,radix 被假定为 8(八进制)或 10(十进制)。具体选择哪一个 radix 取决于浏览器实现,否则radix为10。ECMAScript 5 规范不再允许 parseInt 函数的实现环境把以 0 字符开始的字符串作为八进制数值,但是直至 2013 年,很多实现环境并没有采取新的规范所规定的做法,而且由于必须兼容旧版的浏览器,所以永远都要明确给出 radix 参数的值。将一个数字转换为特定的 radix 中的字符串字段,请使用 thatNumber.toString(radix) 函数。parseInt不应替代 Math.floor(),因为parseInt是向原点取整。
number原型方法(直接使用数值字面量点属性访问原型方法会被理解为小数点而抛出语法错误,需要使用两个点或者中括号属性访问):
(1)Number.prototype.toExponential([fractionDigits]) 方法以指数表示法返回该数值字符串表示形式。用来指定小数点后四舍五入后有fractionDigits位数字,默认情况下用尽可能多的位数来显示数字。对number字面量使用 toExponential() 方法,且该数值没有小数点和指数时,应该在该数值与该方法之间隔开一个空格,以避免点号被解释为一个小数点。也可以使用两个点号调用该方法。
(2)Number.prototype.toFixed(digits) 方法使用定点表示法来格式化一个数值,在必要时进行四舍五入,另外在必要时会用 0 来填充小数部分,以便小数部分有指定的位数digits,介于 0 到 20(包括)之间,超过抛出RangeError错误,如果忽略该参数,则默认digits为 0。在一个非number类型的对象或number原始值上调用,会抛出TypeError错误。如果数值大于 1e+21,该方法会简单调用 Number.prototype.toString()并返回一个指数记数法格式的字符串。浮点数不能精确地用二进制表示所有小数,即0.1 + 0.2 === 0.3 返回 false(计算过程中,0.1和0.2转换分别成二进制后变成无限循环小数,同时截掉多标准位数后面的(截取时采取的是二进制舍入,即1入0舍),精度损失导致相加之和二进制转回十进制变成0.30000000000000004,这种不精确可能会发生在小数的存储/运算/显示的情况,解决方案:parseFloat((0.1 + 0.2).toFixed(10)))。
银行家舍入法的规则是 “四舍六入五考虑,五后非零即存在不是零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一”。由于(2.55).toFixed(1) = 2.5、(3.55).toFixed(1) = 3.5 ,很明显不满足“五前为奇要进一”,toFixed采用的不是银行家舍入法(参考esma规范:如果x < 1021小于,找到n使得n÷10digits - this精确数学值尽可能接近零,如果n存在两个,取较大的n)。
(3)Number.prototype.toLocaleString([locales[, options]]) 方法返回这个数字在特定语言环境下的表示字符串。在使用具有 Intl.NumberFormat API 支持的实现时,此方法仅仅简单调用了 Intl.NumberFormat。比如可以将number使用逗号进行千分位分割。
(4)Number.prototype.toPrecision(precision) 方法以指定的有效数字个数precision四舍五入返回以定点表示法或指数表示法表示的一个数值对象的字符串表示。如果忽略 precision 参数,则该方法表现类似于 Number.prototype.toString()。如果该参数是一个非整数值,该参数将会向下近似到最接近的整数。如果 precison 参数不在 [1, 100]之间,将会抛出一个 RangeError 。
(5)Number.prototype.toString(radix) 方法返回指定 Number 对象的字符串表示形式,覆盖了 Object.prototype.toString()。指定要用于数字到字符串的转换的基数 (从 2 到 36)。如果未指定 radix 参数,则默认值为 10。如果 toString() 的 radix 参数不在 2 到 36 之间,将会抛出一个 RangeError。如果对象是负数,则会保留负号,返回的字符串不是数值的二进制补码,而是包含一个负号(-)前缀和正数的二进制表示。进行数字到字符串的转换时,建议用小括号将要转换的目标括起来,防止出错。
(6)Number.prototype.valueOf() 方法返回一个被 Number 对象包装的原始值。该方法通常是由 JavaScript 引擎在内部隐式调用的,而不是由用户在代码中显式调用的。
9.6 String
String 全局对象是一个用于字符串或一个字符序列的构造函数,继承自Function。从ES6开始,字符串字面量也可以是模板字面量。除普通字符外,特殊功能的字符通过转义字符可出现在字符串字面量中。和其他语言不同,javascript 的字符串不区分单引号和双引号。 对于长字符串,使用 + 运算符将多个字符串连接起,或者,在每行末尾使用反斜杠字符(“\”)并确保反斜杠后面没有空格或任何除换行符之外的字符或缩进,以指示字符串将在下一行继续。
当字符串字面量需要调用一个字符串对象才有的方法或者查询值的时候(字符串字面量是没有这些方法的),JavaScript 会自动将字符串字面量转化为字符串对象并且调用相应的方法或者执行查询。当使用 eval时,字符串字面量被看作源代码处理; 而字符串对象则被看作对象处理,返回对象。
使用 String() 构造器将其它对象转化为字符串比 .toString() 更安全,因为针对 null(undefined),String() 方法也能转换为字符串null(undefined),.toString() 会因为 null或undefined没有属性和方法抛出TypeError错误。
string 类型是不可变的,无论调用何种方法,都不会对值有改变。
String构造器的静态方法:
- String.fromCodePoint(num1[, ...[, numN]]) 静态方法(ES6)返回使用指定的代码点(一串 Unicode 编码位置)序列创建的字符串。如果传入无效的 Unicode 编码,将会抛出一个RangeError。
- String.raw(callSite, ...substitutions) 是一个模板字符串的标签函数,它的作用类似于 Python 中的字符串前缀 r 和 C# 中的字符串前缀 @。是用来获取一个模板字符串的原始字符串,即结果。如果第一个参数没有传入一个格式正确的对象callSite,则会抛出 TypeError 异常。也能以String.raw`templateString`的方式调用。
- String.fromCharCode(num1[, ...[, numN]]) 方法返回由指定的 UTF-16 代码单元(范围介于 0 到 65535(0xFFFF)之间,ASCII码在其中)序列创建的字符串。因此可用于将数字转换为ASCII码字符。
- 字符串的 length 只读属性表示字符串的 UTF-16 代码单元(简称码元)数量,不包括转义字符\,其属性特性writable(可重写)、enumerable(可枚举)、configurable(可配置)均为false。由于每个 Unicode 字符可以编码为一个或两个代码单元(表情符号、特殊的汉字等)。语言规范要求字符串的最大长度为 253 - 1 个元素,这是精确整数的上限。但是,具有此长度的字符串需要 16384TB 的存储空间,因此浏览器实现的大多不超过231 - 1(4GB)。如果要统计字符数量,建议先用它的迭代器分割字符串[...str],再访问[...str].length。而且若尝试修改字符串的length,非严格模式下没有效果,严格模式下会抛出TypeError错误。
String原型方法:
- String.prototype[@@iterator]()方法返回一个新的 Iterator 对象,它遍历字符串的字符。可以通过for..of...自动调用[@@iterator]() 。
- String.prototype.at(index) 方法接受一个整数值index(允许正整数和负整数。负整数从字符串中的最后一个字符开始倒数,默认值为0),并返回一个新的 String,该字符串由位于指定偏移量处的单个 UTF-16 码元(不一定和字符对应)组成。如果找不到则返回undefined。
- String.prototype.charAt(index) 方法返回指定index([0, length-1],默认值为0)位置的UTF-16 码元。如果找不到则返回空字符串。
- String.prototype.charCodeAt(index) 方法返回给定索引index(默认为0)位置的UTF-16 代码单元对应的整数。index超过范围则返回NaN。
- String.prototype.codePointAt(index) 方法返回一个 Unicode 编码点值(码点)的非负整数。如果在索引处开始没有 UTF-16 代理对,将直接返回在那个索引处的编码单元,如果既没有代理对也没有代码单元则返回 undefined。代理对是UTF-16 中用于扩展字符而使用的编码方式,是一种采用四个字节 (两个 UTF-16 编码) 来表示一个字符。
- String.prototype.concat(str2, [, ...strN]) 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。concat 方法并不影响原字符串。如果参数不是字符串类型,它们在连接之前将会被转换成字符串。强烈建议使用赋值运算符和 + 代替concat方法。
- String.prototype.endsWith(searchString[, length]) 方法用来判断当前字符串str是否是以另外一个给定的子字符串searchString 在参数length - 1位置处“结尾”的,对大小写敏感,根据判断结果返回 true 或 false。参数length默认值为 str.length。
- String.prototype.includes(searchString[, position]) 方法执行区分大小写的搜索,以确定当前字符串str从position位置(默认为0)开始是否包含被搜寻的字符串searchString,并根据情况返回 true 或 false。如果 searchString 是一个正则表达式,抛出TypeError错误。
- String.prototype.indexOf(searchString[, position])指定子字符串在大于或等于 position 位置(position不传或小于0,则position为0,position大于length - 1则方法返回-1)的第一次出现的索引。找不到则返回-1。如果不带参数调用方法,searchString 将被强制转换为 "undefined"。大小写敏感。
- String.prototype.lastIndexOf(searchValue[, fromIndex]),待匹配字符串 searchValue 从 str 的第 fromIndex 位开始向左回向查找最后一次出现的位置。fromIndex默认值是 +Infinity。如果 fromIndex >= str.length ,则会搜索整个字符串。如果 fromIndex < 0 ,则等同于 fromIndex == 0。大小写敏感。
- String.prototype.localeCompare(compareString[, locales[, options]]),返回一个数字表示是否引用字符串 str在字典排序中位于 比较字符串compareString 的前面,后面,或者二者相同。如果引用字符存在于比较字符之前则为负数; 如果引用字符存在于比较字符之后则为正数; 相等的时候返回 0 。
- String.prototype.match(regexp),如果传入一个非正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp。如果没有给出任何参数,则返回包含一个空字符串的数组[""] 。如果未找到匹配则为null。如果使用 g 标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。如果未使用 g 标志,则仅返回一个数组(Array)包括第一个完整匹配及正则所匹配的匿名捕获组和以下属性:
- groups: 一个命名捕获组对象,其键是捕获组名称,值是捕获组,如果未定义命名捕获组,则为 undefined。
- index: 匹配的结果的开始位置。
- input: 搜索的字符串。
- String.prototype.matchAll(regexp),方法返回一个包含所有匹配正则表达式regexp的结果及分组捕获组的迭代器。如果所传参数regexp不是一个正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp。RegExp必须是设置了全局模式g的形式,否则会抛出异常TypeError。
- String.prototype.normalize([form])会按照指定的一种 Unicode 正规形式(四种 Unicode 正规形式 "NFC"、"NFD"、"NFKC",或 "NFKD" 其中的一个,默认值为 "NFC",如果给 form 传入了上述四个字符串以外的参数,则会抛出 RangeError 异常)将当前字符串规范化。如果该值不是字符串,则首先将其转换为一个字符串。
- String.prototype.padStart/padEnd(targetLength[, padString]) 返回在原字符串str开头/末尾填充指定的填充字符串padString直到达到目标长度后形成的新字符串。如果需要,padString可以重复填充,而如果padString太长,则只保留padString的左侧。padString默认值为空格。
- String.prototype.repeat(count)构造并返回一个指定重复次数count的新字符串。count是介于0 和 +Infinity的整数,重复次数不能为负数且重复次数必须小于 infinity,且长度不会大于浏览器允许的最长的字符串,否则抛出RangeError错误。原字符串不会改变。实现
- String.prototype.slice(beginIndex[, endIndex]),提取字符串str的一部分,并返回一个新的字符串,且不会改动原字符串。beginIndex默认为0,endIndex默认为strLength;如果beginIndex或endIndex的值为负数,会被当做 strLength + beginIndex或 strLength + endIndex。
- String.prototype.split([separator[, limit]),使用指定的分隔符字符串或正则表达式separator(如果不是字符串或正则表达式,会强制转为字符串)将一个String对象分割成子字符串数组。limit限定返回的分割片段数量。如果省略separator或在 str 中不出现分隔符,则返回的数组仅包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将 str 原字符串中每个字符的数组形式返回,如果字符串和分隔符都是空字符串,则返回一个空数组。
- String.prototype.startsWith(searchString[, position])方法(ES6)用来判断当前字符串str是否在position(默认为0)的位置开始以给定字符串searchString开头,并根据判断结果返回 true 或 false。
- String.prototype.substring(indexStart[, indexEnd]) 方法提取从 indexStart (默认为0)到 indexEnd(不包括,默认为length)之间的字符构成的字符串。如果 indexStart 等于 indexEnd,substring 返回一个空字符串。如果任一参数小于 0 或为 NaN,则被当作 0。 如果任一参数大于 str.length,则被当作 str.length。如果 indexStart 大于 indexEnd则调换两个参数。
- String.prototype.toLocaleLowerCase()/String.prototype.toLocaleUpperCase()方法返回根据任意区域语言大小写映射集而转换成小/大写格式的字符串。参数 locale 指明要转换成小/大写格式的特定语言区域。如果以一个数组 Array 形式给出多个 locales, 最合适的地区将被选出来应用。原始字符串不变。在大多数情况下,和调用 String.prototype.toLowerCase()的结果相同,但是在某些区域环境中(比如土耳其语),大小写映射并不遵循在 Unicode 中的默认的大小写映射,两者结果不同。如果locale是无效的语言标记,会抛出RangeError错误。如果数组 Array 形式中的locale不是字符串形式,会抛出TypeError错误。
- String.prototype.toLowerCase()/String.prototype.toUpperCase()将调用该方法的字符串转为小/大写形式的新字符串并返回(如果调用该方法的值不是字符串类型会被强制转换)。在 null 或 undefined类型上调用会抛出TypeError错误。
- String.prototype.trim()/String.prototype.trimStart()/String.prototype.trimEnd() 方法从字符串的两端/首/末尾清除空格(所有的空白字符(空格、tab、不换行空格等)以及所有行终止符字符(如 LF、CR 等)),返回一个新的字符串,而不修改原始字符串。trimLeft/trimRight是方法trimStart/trimEnd的别名。
- String.prototype.toString(),返回String 包装对象的字符串原始值。覆盖了 Object 对象的 toString() 方法。与String.prototype.valueOf() 方法的返回值相同。
- String.prototype.valueOf()方法返回String 包装对象的字符串原始值。
- String.prototype.search(regexp)方法执行正则表达式regexp(如果传入一个非正则表达式对象 regexp,则会使用 new RegExp(regexp) 隐式地将其转换为正则表达式对象。)和 String 对象之间的一个搜索匹配。如果匹配成功,则 search() 返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1。
- String.prototype.replace(pattern, replacement)方法返回一个由替换值(replacement)替换部分或所有的模式(pattern)匹配项后的新字符串。模式可以是一个字符串或者一个正则表达式,如果pattern是字符串,则仅替换第一个匹配项。如果pattern是正则表达式,所匹配的内容会被第二个参数的返回值替换掉。替换值可以是一个字符串或者一个每次匹配都要调用的回调函数,如果replacement是字符串,可以使用一些特殊变量名,比如$n表示第n个捕获组匹配的结果。原字符串不会改变。
- String.prototype.replaceAll(pattern, replacement)方法返回一个新字符串,新字符串所有满足 pattern 的部分都已被replacement 替换。pattern可以是一个字符串或一个 RegExp, replacement可以是一个字符串或一个在每次匹配被调用的函数。 当pattern是 regex 时,必须设置全局(“g”)标志, 否则,它将引发 TypeError错误。原字符串不会改变。
实现字符串翻转:
零宽字符(隐藏字符)
- U+200:零宽度空格符,用于较长单词的换行分隔;
- U+FEFF:零宽度非断空格符,用于阻止特定位置的换行分隔;
- U+200D : 零宽度连字符,用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果;
- U+200C : 零宽度断字符,用于阿拉伯文,德文,印度语系等文字中,阻止会发生连字的字符间的连字效果;
- U+200E : 左至右符,用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语) ,规定排版文字书写方向为左至右;
- U+200F : 右至左符 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左。
应用场景:
- 文字水印:对于一些对版权要求很严格的网站,将文章访问用户名转换为零宽字符,隐藏在文章内容中,既不影响阅读,也可以标记了用户信息,一旦文章内容泄漏,直接解码中用户名即可了解到信息泄漏的源头。
- 逃脱关键词匹配:将零宽字符放在关键词中间,即可逃脱部分关键词匹配程序。
- 隐藏信息传递:以特定排列顺序表示隐藏信息,达到传递信息和加密、解密信息的目的。
模板字面量
模板字面量是用反引号(`)分隔的字面量,允许多行字符串(`string text line 1
string text line 2`,因为在源码中插入的任何换行符都是模板字面量的一部分)、带嵌入表达式的字符串插值(在反引号分隔的模板中,允许在占位符 ${expression} 中使用内层的反引号进行模板字面量的嵌套)和一种叫带标签的模板的特殊结构。
语法:tagFunction`string text ${expression} string text`,其中string text 将成为模板字面量的一部分的字符串文本,几乎允许所有字符,包括换行符和其他空白字符。但是,除非使用了标签函数,否则无效的转义序列将导致语法错误。expression是可选的要插入当前位置的表达式,其值被转换为字符串(和普通的字符串加法拼接的区别:模板字面量直接将expression表达式强制转换为字符串,而加法则会先将操作数强制转换为原始类型再进行字符串拼接)或传递给可选的tagFunction。tagFunction 如果指定,将使用模板字符串数组(作为第一个参数传递,对于任何模板字面量,其长度等于替换次数(${…} 出现次数)加一,因此总是非空的)和替换expression表达式(分别作为第2 - n个参数传递)调用它,而且,对于任何特定的带标签的模板字面量表达式,无论对字面量求值多少次,都将始终使用完全相同的字面量数组调用标签函数,因此这允许标签函数以其第一个参数作为标识来缓存结果,而且为了进一步确保数组值不变,这个字符串数组参数及其 raw 属性都会被冻结,使得将无法改变它们。标签函数的返回值结果被称为带标签的模板,将成为模板字面量的值,标签函数甚至不需要返回字符串!
在标签函数的第一个参数即字符串数组中,存在一个特殊的属性 raw,可以通过它来访问模板字符串的原始字符串(即源码字符串),而无需转义特殊字符。
若要转义模板字面量中的反引号(`),需在反引号之前加一个反斜杠(\)。美元符号 $ 也可以被转义,来阻止插值。
带标签的模板允许使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组,其余的参数与expression表达式相关。可以用标签函数对这些参数执行任何操作,并返回被操作过的字符串(或者,也可返回完全不同的内容)。用作标签的函数名没有限制,不必是普通的标识符,可以使用任何运算符优先级大于 16 的表达式,包括属性访问、函数调用(唯一的例外是标签函数存在可选链,这将抛出语法错误:SyntaxError: Invalid tagged template on optional chain)、new 表达式,甚至其他带标签的模板字面量。对于不带标签的模板字面量,虽然语法从技术上允许作为标签,但由于它是字符串,所以在链式调用时会抛出 TypeError。
另外,使用 String.raw() 方法创建原始字符串和使用默认模板函数和字符串连接创建是一样的。
模板字面量有时被非正式地叫作模板字符串,因为它们最常被用作字符串插值(通过替换占位符来创建字符串)。然而,带标签的模板字面量可能不会产生字符串——它可以与自定义标签函数一起使用,来对模板字面量的不同部分执行任何操作。
9.7 Boolean
Boolean 对象是一个布尔值的对象包装器,继承自Function。基本类型(原始值)中的布尔值 true 和 false 与值为 true 和 false 的 Boolean 对象是不同的,任何Boolean对象传递给条件语句都是true。
当Boolean()作为构造函数调用时(使用new),它会创建一个Boolean对象,该对象不是原始值,尽量不将其作为构造函数使用来转换值,而是应该直接将 Boolean 当做转换函数即布尔包装器来使用,或者使用双重非(!!)运算符。
当将非标准属性 document.all 用作此构造函数的参数时,结果是值为 false 的布尔对象。此属性是旧的、非标准的属性,不应使用。
当Boolean()作为函数调用时(没有new),即布尔包装器,它会将参数强制转换为布尔原始值。
原型方法:
- Boolean.prototype.toString():覆盖了Object.prototype.toString(),根据对象的值返回字符串 true 或 false。
- Boolean.prototype.toString():覆盖了Object.prototype.valueOf(),返回 Boolean 对象的原始值。
9.8 null
在 JavaScript中,null 是基本类型(原始值),特指对象的值未设置,在布尔运算中被认为是falsy值。
null 是一个字面量,不像 undefined,它不是全局对象的一个属性。
9.9 undefined
undefined是全局对象的属性,值为基本类型(原始值)undefined。一个声明未定义的变量的初始值,或没有实际参数的形式参数,以及方法或者是语句中操作的变量没有被赋值,则会返回 undefined。其属性特性writable(可重写)、enumerable(可枚举)、configurable(可配置)均为false。undefined 不是一个保留字,因此可能被赋值,而 void 表达式可以返回安全的undefined。
一个变量是否被声明可以通过看它是否在一个封闭的上下文中被声明,比如使用typeof variable === undefined 或 variable === void 0。唯一的例外是全局作用域,但是全局作用域是被绑定在全局对象上的,所以要检查一个变量是否在全局上下文中存在可以通过检查全局对象上是否存在这个属性。
9.10 BigInt
内置对象BigInt 提供表示大于 2^53 - 1 的任意大整数,包装器函数BigInt()继承自Function。BigInt没有负零(-0n)。定义 BigInt原始值的方式包括:
- 在一个整数字面量后面加 n,是bigInt原始值。
- 调用包装器函数BigInt()(BigInt() 不能与 new 运算符一起使用,会抛出TypeError错误)并传递一个整数值或字符串值。
与Number有区别:
- 不能使用 Math 对象的方法;
- 不能与 Number 混合运算,需要转换为相同类型,且 BigInt 在转换成 Number 时可能会丢失精度(因此建议仅在值可能大于 2^53 - 1时使用 BigInt 类型,并且不在两种类型之间进行相互转换)。但是可以使用自增/自减运算符。
- 由于对 BigInt 的操作不是常数时间的,因而 BigInt 不适合用于密码学。
由于 BigInt 都是有符号的, >>> (无符号右移)不能用于 BigInt。当使用 BigInt 时,带小数的运算会被向下取整。相同的数值的BigInt字面量和Number字面量不严格相等(类型不同)。
对任何 BigInt 值使用 JSON.stringify() 都会引发 TypeError,因为默认情况下 BigInt 值不会在 JSON 中序列化。但是,如果需要,可以利用.toString()在BigInt的prototype上实现 toJSON 方法。
BigInt包装函数的静态方法:
- BigInt.asIntN(width, bigint)方法方法通过将 bigint 模 2^width转换为 -2^(width-1) 与 2^(width-1)-1 之间的的有符号整数。
- BigInt.asUintN (width, bigint)方法通过将 bigint 模 2^width转换为一个 0 和 2^width-1 之间的的无符号整数。
BigInt原型方法:
- BigInt.prototype.toLocaleString() 方法返回一个具有此 BigInt 在特定语言环境的且不包含"n"的表达形式。
- BigInt.prototype.toString([radix])方法返回一个表示指定 BigInt 对象的不包含"n"字符串。重写 Object 对象的 toString() 方法。radix可选,是介于 2 到 36 之间的整数,默认为10,指定用于表示数值的基数,如果 radix小于 2 或大于 36, 则抛出 RangeError。如果bigIntObj是负数,则会保留负号,返回的字符串不是数值的二进制补码,而是包含一个负号(-)前缀和正数的二进制表示。
- BigInt.prototype.valueOf()返回 BigInt 对象包装的bigInt原始值。
9.11 Symbol
symbol是基本数据类型,Symbol([description])函数会返回唯一的 symbol 类型的值,每次调用Symbol都是创建一个新的symbol值,它不支持"new Symbol()"语法(抛出不是构造函数的 TypeError 错误),其中description是字符串类型,仅仅表示对 symbol 的描述。symbol 值能作为对象的属性的键或值;从ES6开始不支持通过原始数据类型创建一个显式包装器对象(ES6 之前的new Boolean、new String以及new Number除外),包括BigInt和Symbol,只能以symbol值或BigInt值为参数调用Object() 函数来创建。
要创建跨文件可用的 symbol,甚至跨域(每个都有它自己的全局作用域),使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的 symbol 注册表设置和取得 symbol,而因为Symbol()创建的不是全局共享的symbol值。
以 symbol 值作为键的属性会被JSON.stringify()、for...in、Object.getOwnPropertyNames(obj)忽略,可以通过Object.getOwnPropertySymbols(obj) 获取给定对象obj自身的所有 Symbol 属性的数组。Symbol 包装器对象Object(sym)作为属性地键时会被强制转换为该symbol 值。
对symbol值进行类型转换:
- 尝试显示或隐式地将symbol 值转换为number 值会抛出 TypeError 错误;
- Object(sym) == sym 为true;
- 尝试隐式地将symbol值转换为string值会抛出TypeError错误;
- 使用String(sym)显示地将symbol值转换为string值和Symbol.prototype.toString()效果相同,返回"Symbol(description)"。
Symbol函数的静态属性和静态方法,其属性特性writable(可重写)、enumerable(可枚举)、configurable(可配置)均为false:
- Symbol.asyncIterator 属性用于指定一个对象的默认异步迭代器,定义该属性后该对象才能成为异步可迭代对象,才能用于for await...of循环,对应于@@asyncIterator符号。目前没有设定该属性的内置对象。
- Symbol.hasInstance用于判断某对象是否为某构造器的实例。可以用它作为静态方法自定义 instanceof 操作符在某个类上的行为。
- Symbol.isConcatSpreadable用于配置某对象作为Array.prototype.concat()方法的参数时是否展开其数组元素,对于数组默认为true,而对于类数组默认为false,对应于@@isConcatSpreadable符号。
- Symbol.iterator 属性用于指定一个对象的默认迭代器,定义该属性后该对象才能成为可迭代对象,才能用于for...of循环,用于展开运算符迭代,对应于@@iterator符号。拥有迭代器的内置类型包括Array.prototype、TypedArray.prototype、String.prototype、Map.prototype、Set.prototype。如果一个迭代器 @@iterator 没有返回一个迭代器对象,那么它就是一个不符合标准的迭代器。这样的迭代器将会在运行期抛出异常。
- Symbol.match 指定是否具有正则表达式的行为,对应于@@match符号。String.prototype.startsWith(),String.prototype.endsWith() 和 String.prototype.includes() 这些方法会检查其第一个参数是否是正则表达式,是正则表达式就抛出一个TypeError。可以通过对参数设置Symbol.match为false(或falsy值),让参数失去正则表达式行为从而避免抛出错误。String.prototype.match() 方法会调用@@match方法。
- Symbol.matchAll 属性指定返回一个迭代器的方法,该迭代器根据字符串生成正则表达式的匹配项。此方法可以被 String.prototype.matchAll() 方法调用。
- Symbol.replace 属性指定了当一个字符串替换所匹配字符串时所调用的方法。String.prototype.replace() 方法会调用此方法。
- Symbol.search 指定了一个搜索方法,这个方法接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标。该方法由 String.prototype.search()来调用。
- Symbol.species 访问器(get)属性允许子类覆盖对象的默认构造函数,其返回值即为子类的构造函数。比如对于子类MyArray,Array.prototype.map方法返回的是使用子类默认的构造函数创建的实例对象,通过Symbol.species可以重新子类使用的构造函数为Array,如此一来Array.prototype.map返回的就是Array的实例对象。
- Symbol.split指定在与正则表达式匹配的索引处拆分字符串的方法。该方法由String.prototype.split(reg)调用,即reg如果存在Symbol.split方法则str.split使用该方法处理拆分。
- Symbol.toPrimitive 属性指定了一种接受首选类型hint并返回对象原始值的方法[@@toPrimitive]()。它被所有的强类型转换制算法优先调用。"number" hint 用于强制数字类型转换算法。"string" hint 用于强制字符串类型转换算法。"default" hint 用于强制原始值转换算法。[@@toPrimitive]() 必须返回一个原始值,否则将抛出 TypeError。
- Symbol.toStringTag 作为对象的属性键使用,对应的属性值为字符串类型,用来表示该对象的自定义类型标签, Object.prototype.toString() 方法会去读取该标签并把它包含在返回值里("[object Map]"后面的Map就是类型标签)。许多内置的 JavaScript 类型(String, Number,Array,Boolean,Null,Undefined)即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签。而GeneratorFunction、Map、Set、Promise等等则是设置了Symbol.toStringTag。但自己创建的类如果没有设置get [Symbol.toStringTag] 属性,toString() 找不到 toStringTag 属性时只好返回默认的Object 标签。
- Symbol.unscopables 用于指定一个对象其自身和继承的属性名称是否被排除在with环境绑定之外。在Symbol.unscopables中设置为true的属性名称在with环境中无法访问,而设置为false的属性可访问。
- Symbol.for(key) 方法会根据给定的键 key,来从运行时的全局 symbol 注册表(全局symbol 注册表中的每个记录结构包括 用于标识每个 symbol的字符串[[key]]和存储的 symbol 值的[[symbol]])中找到对应的 symbol,找到则返回,找不到才新建一个与该键key(同时也会作为该 symbol 的描述)关联的symbol,并放入全局 symbol 注册表中,并返回。为了防止冲突,最好给要放入 symbol 注册表中的 symbol 带上键前缀。
- Symbol.keyFor(sym) 方法用来获取全局 symbol 注册表中与某个 symbol 关联的键。如果全局注册表中查找到该 symbol,则返回该 symbol 的 key 值,返回值为字符串类型。否则返回 undefined。
symbol值的原型(Symbol.prototype)属性与方法:
- Symbol.prototype[@@toPrimitive](hint)将 Symbol 对象转换为symbol原始值。Symbol.toPrimitive即@@toPrimitive。当对象需要被转换为原始值时,JavaScript 自动地调用该[@@toPrimitive]() 方法将一个对象转换为原始值表示。
- Symbol.prototype.toString()方法返回当前 symbol 对象的字符串表示(即"Symbol(description)")。重写 Object 对象的 toString() 方法。
- Symbol.prototype.valueof()方法返回当前 symbol 对象的symbol原始值(即"Symbol(description)")。重写 Object 对象的 toString() 方法。