标签: javascript

一、padStart()简介

JavaScript的字符串padStart()方法用于在当前字符串的开头添加指定数量的字符,以达到指定的字符串长度。如果当前字符串的长度大于或等于指定的字符串长度,则不会添加任何字符。

二、语法

string.padStart(targetLength [, padString])

三、参数解释

targetLength:要达到的字符串长度,必须为一个正整数。

padString:可选的填充字符串,如果不指定,则默认为一个空格。

四、使用实例

下面是一些使用padStart()方法的示例:

//小于两位补零
String(date.getMonth() + 1).padStart(2, '0');

//文件名生成
for (let i = 1; i <= 10; i++) {
    const fileName = `file${i.toString().padStart(2, '0')}.txt`;
    console.log(fileName); // 输出: file01.txt file02.txt file03.txt ... file10.txt
}

五、注意事项

1、如果fillString的长度大于targetLength,则会截取fillString的前面部分。

2、如果不指定fillString,则默认为一个空格。

3、如果当前字符串的长度大于或等于目标长度,则不会添加任何字符。

六、常用在哪里

padStart()方法常用于字符串的格式化,比如在输出表格时,确保每列的宽度相同,可以使用padStart()方法在开头添加空格或其他字符。还可以用于格式化日期、时间等。

七、关于padEnd

padEnd()可以在字符串的后面进行字符补全,语法参数等都和padStart()类似。

八、padEnd注意

如果补全字符串长度不足,则从左往右不断循环补全;如果长度超出可以补全的字符长度,则从左侧尽可能补全,补不到的没办法,只能忽略,例如'zhangxinxu'.padEnd(15, {})等同于执行'zhangxinxu'.padEnd(15, '[object Object]'),最多只能补5个字符,因此,只能补'[object Object]'前5个字符,于是最后结果是:'zhangxinxu[obje'

padString参数如果不设置,则会使用普通空格' '(U+0020)代替,也就是Space空格键敲出来的那个空格。

九、padEnd案例

在JS前端我们处理时间戳的时候单位都是ms毫秒,但是,后端同学返回的时间戳则不一样是毫秒,可能只有10位,以s秒为单位。所以,我们在前端处理这个时间戳的时候,保险起见,要先做一个13位的补全,保证单位是毫秒。使用示意:

timestamp = +String(timestamp).padEnd(13, '0');

十、兼容性

padStart()padEnd()是属于ES8(ES2017)的方法,兼容性:IE不支持,Chrome版本≥57

一、前端发展阶段

1. 静态页面阶段

互联网发展的早期,网站的前后端开发是一体的,即前端代码是后端代码的一部分。

  1. 后端收到浏览器的请求
  2. 生成静态页面
  3. 发送到浏览器

那时的前端页面都是静态的,所有前端代码和前端数据都是后端生成的。前端只是纯粹的展示功能,脚本的作用只是增加一些特殊效果,比如那时很流行用脚本控制页面上飞来飞去的广告。

那时的网站开发,采用的是后端 MVC 模式。

  • Model(模型层):提供/保存数据
  • Controller(控制层):数据处理,实现业务逻辑
  • View(视图层):展示数据,提供用户界面

前端只是后端 MVC 的 V。

2. AJAX 阶段

2004年,AJAX 技术诞生,改变了前端开发。Gmail 和 Google 地图这样革命性的产品出现,使得开发者发现,前端的作用不仅仅是展示页面,还可以管理数据并与用户互动。

AJAX 技术指的是脚本独立向服务器请求数据,拿到数据以后,进行处理并更新网页。整个过程中,后端只是负责提供数据,其他事情都由前端处理。前端不再是后端的模板,而是实现了从“获取数据 --》 处理数据 --》展示数据”的完整业务逻辑。

就是从这个阶段开始,前端脚本开始变得复杂,不再仅仅是一些玩具性的功能。

3. 前端 MVC 阶段

前端代码有了读写数据、处理数据、生成视图等功能,因此迫切需要辅助工具,方便开发者组织代码。这导致了前端 MVC 框架的诞生。

2010年,第一个前端 MVC 框架 Backbone.js 诞生。它基本上是把 MVC 模式搬到了前端,但是只有 M (读写数据)和 V(展示数据),没有 C(处理数据)。因为,Backbone 认为前端 Controller 与后端不同,不需要、也不应该处理业务逻辑,只需要处理 UI 逻辑,响应用户的一举一动。所以,数据处理都放在后端,前端只用事件响应处理 UI 逻辑(用户操作)。

后来,更多的前端 MVC 框架出现。另一些框架提出 MVVM 模式,用 View Model 代替 Controller。MVVM 模式也将前端应用分成三个部分。

  • Model:读写数据
  • View:展示数据
  • View-Model:数据处理

View Model 是简化的 Controller,所有的数据逻辑都放在这个部分。它的唯一作用就是为 View 提供处理好的数据,不含其他逻辑。也就是说,Model 拿到数据以后,View Model 将数据处理成视图层(View)需要的格式,在视图层展示出来。

这个模型的特点是 View 绑定 View Model。如果 View Model 的数据变了,View(视图层)也跟着变了;反之亦然,如果用户在视图层修改了数据,也立刻反映在 View Model。整个过程完全不需要手工处理。

4. SPA 阶段

前端可以做到读写数据、切换视图、用户交互,这意味着,网页其实是一个应用程序,而不是信息的纯展示。这种单张网页的应用程序称为 SPA(single-page application)。

SPA是指在一张网页(single page)上,通过良好的体验,模拟出多页面应用程序(application)。用户的浏览器只需要将网页载入一次,然后所有操作都可以在这张页面上完成,带有迅速的响应和虚拟的页面切换。

随着 SPA 的兴起,2010年后,前端工程师从开发页面(切模板),逐渐变成了开发“前端应用”(跑在浏览器里面的应用程序)。

前端领域风起云涌,框架层出不穷。目前,前端三大马车 Vue、Angular、React ,都属于 SPA 开发框架。

同时我们也注意到在众多前端框架中,由 Rich Harris (Ractive, Rollup 和 Bubble 的作者) 开发的 Svelte 有较于 React 更小的体积、更高的开发效率和性能,有望成为一批黑马,在前端框架中脱颖而出。

二、前端新标准

2014年,W3C正式发布HTML5.0标准让前端技术蓬勃发展。HTML6.0目前处于提案阶段。

Web1.0到2.0的转变,由静态互联网过渡到Web应用程序,极大地改变了前端技术。

Web3.0时代,可能是去中心化,可能是物联网,可能是人工智能,值得每个前端开发去关注。

三、框架选择

1、视图框架

React,Vue,Angular 是前端强势铁三角。由于新版本推出如Vue3.x和React18,占有率会变得更高。

2、UI 框架

由于模块化CSS、摇树、MVVM 的流行,UI 框架的选择其实没有那么重要了。适合的UI才是好的UI。

四、新工具的诞生和发展

1. 构建工具

随着nodejs和Babel的成功,构建工具也有了很多的突破。打包器大概可以分为传统编译和 ESM 混合编译,从 webpack 到 vite ,说明原生 ES 模块的接纳一直在继续。

Vite: “快”是它的核心,它主要解决的痛点就是项目开发启动缓慢。

ESM 混合编译:在开发环境编译时,使用 Server 动态编译 + 浏览器的 ESM,基本上实现了“开发环境 0 编译”的功能。而生产环境编译时,则会调用其他编译工具来完成(如 Vite 使用 Rollup)。

另一方面,出于对性能的考虑,越来越多的前端工具开始用其他语言 (Rust、Go) 来构建。

2. 混合式开发工具

将JavaScriptCore引擎当做虚拟机的方案,代表框架React Native;

另一种是使用非JavaScriptCore虚拟机的方案,代表框架是Flutter。相比于 React Native框架, Flutter的优势最主要体现在性能、开发效率和体验这两大方面。但从编程语言角度来说,JavaScript的历史和流行程度都远超Dart,生态也更加完善,开发者也远多于Dart程序 员。所以,从编程语言的角度来看,虽然Dart语言入门简单,但从长远考虑,还是选择 React Native会更好一些。

另外为了使用H5开发移动端应用,使得相应出现了一些混合式开发(Hybrid App)的构建工具,如uniapp、Taro等。

3. 桌面应用

  • Electron: 我们的老熟人,Chromium + Nodejs,深受大家喜爱
  • Tauri: 异军突起的新星,Webview + Rust (可替换)。对比 Electron 因为不用打包 Chromium 和 Nodejs 运行时,产物体积小,内存占用小,运行性能好

4. 类型化语言

TypeScript 使用率近年来稳健增长,目前已经是前端项目的标配了,可以预计未来将会有更多强大的配套工具提高生产力还有提升使用体验。

5. 包管理工具

从 yarn 再到现在的 pnpm,解决了很多npm存在的诟病。

pnpm 可以说是 npm 的超级加强版,或者 yarn 的加强版。其独特的软连接和硬链接的设计,使得装依赖很快(Yarn 从缓存中复制文件,而 pnpm 只是从全局存储中链接它们),解决了多个项目依赖包缓存问题,避免了重复下载。

但是这类 npm 代替工具,也不是非用不可,因为 npm 自身也在不断地优化。比如早期 npm 不支持 package-lock.json 的时候,大家都说 “Yarn 简直太有用了!”,但是后来 npm 支持了 package-lock.json 文件后,二者的差距就很小了。

什么是pnpm以及pnpm的安装与使用

五、智能平台

1. 低代码平台

低代码发展初期,厂商的类型多样化,传统软件厂商、互联网大厂均涉及低代码领域,通用型厂商相对垂直型厂商应用场景更加广泛,因此厂商数量更多。但随着市场成熟,通用类型厂商竞争加剧,垂直型厂商在细分的领域将会呈现优势明显趋势,可以进一步挖掘用户场景,提升产品能力和用户满意度。

及早布局低代码产业生态,多维度拓展厂商优势,才能在未来占据高地。

2. AI 与图形化的探索

人工智能作为跨时代技术在各个领域大放异彩,近些年 AI 能力在前端领域的尝试与应用带来新一轮的技术革命。

游戏引擎和3D技术的不断发展,给前端注入了新的活力。

阿里的 Imgcook 可以通过识别设计稿(Sketch / PSD /图片)智能生成 React、Vue、Flutter、小程序等不同种类的前端代码,目前可以生成 70% 以上的代码,智能生成代码不再只是一个线下实验产品,而是真正产生了价值。但这个只能说是减轻静态页面的工作量而已,无法取代前端。

六、跨平台技术

随着从 PC 时代向移动互联网时代演进,原生客户端因为自身天花板的原因也在逐渐向跨平台方案倾斜,当然这得益于跨平台方案的明显优势。对于开发者而言,可以做到一次开发多端复用,这在很大程度上能够降低研发成本,提高产品效能。但是,移动端的跨平台技术并不是仅仅考虑一套代码能够运行在不同场景即可,还需要解决性能、动态性、研发效率以及一致性的问题。

1. React Native 和 Flutter

React Native 是以 Web 技术开发原生移动应用的典型框架。但是与众多基于 Html 的跨平台框架相比,Flutter 绝对是体验最好,性能与构建思路几乎最接近原生开发的框架。

Flutter 目前虽然有着跨端最好的性能和体验但是关注人数和 React Native 不相上下。React Native 由于先出优势加上 React 的影响力和 Flutter 的学习成本(Dart语言)导致目前很多 APP 都已经进入存量阶段,少有新的 APP 出现,所以在没有足够的收益情况下,大部分 APP 是不会进行技术变更的。

2. 小程序

目前主流小程序平台有很多,包括:

  • 腾讯的微信小程序、QQ 小程序;
  • 阿里的支付宝小程序、淘宝轻店铺;
  • 字节跳动的头条小程序、抖音小程序;
  • 百度小程序等;

不同平台的实现标准各不相同,开发者需要学习不同平台的开发规范做定制化开发。所以在 2019 年 9 月阿里巴巴主导发起并联合 W3C 中国及国内外厂商起草了 MiniApp 标准化白皮书(MiniApp Standardization White Paper),旨在制定共同标准减少平台差异,并成立了相关工作组。

但从目前来看各平台对这些标准的实现度还很低,并未实现统一,所以这么来看标准化的路看着还很长。在当下,解决跨平台开发问题最有效的手段是使用转换框架进行转换

随着一些跨端框架(Uniapp、Taro)的出现部分跨端转换器基本已经定型。另外还有其他一些跨端转换器相关的内容:

  • wept: 微信小程序实时运行工具,目前支持 Web、iOS 两端的运行环境;
  • hera-cli: 用小程序方式来写跨平台应用的开发框架,可以打包成 Android 、 iOS 应用,以及 H5;
  • weweb-cli: 兼容小程序语法的前端框架,可以用小程序的写法来写 web 应用

跨端这项技术并非为了完全替代原生开发,针对每个场景我们都可以用原生写出性能最佳的代码。但是这样做工作量太大,实际项目开发中需要掌握效率与优化之间的平衡。跨端的优势在于能让我们在尽可能保障性能的前提下书写更有效率的代码

七、泛前端方向

“前端开发”的发展历史像是一直在找寻自己的定位;从切图仔、写 HTML 模板的“石器时代”,到前后端分离、大前端的“工业时代”,再到现在跨端技术、低代码的“电气时代”。前端研发的职责一直在改变,同时前端研发需要掌握的技术也在迭代更新。

1. 全栈

“全栈开发者”是指“同时掌握前端、后端以及其他网站开发相关技能的开发者”。全栈开发者能够胜任产品开发的全流程,从前端到后端,从架构设计到代码落地,从自动化测试到运维等。对于公司来说,全栈工程师可以减小公司的用人成本,减少项目沟通成本;对于个人来说,拥有全链路技术有益于技术的闭环,扩展全局思维,提升个人能力和价值。

2. DevOps

DevOps(Development 和 Operations 的组合词)是一种重视“软件开发人员(Dev)”和“IT 运维技术人员(OPS)”之间沟通合作的模式。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。可以把 DevOps 理解为一个过程、方法与系统的总称。

在Docker自动化部署和流水线发版工具的发展下,DevOps越来越被开发人员所重视。

八、5G场景带来的新趋势

5G 的到来对于技术研发工作者的我们而言意味深远。它的出现是数据传输速度、响应速度和连接性的一次巨大飞跃。

5G 将与超高清视频、VR、AR、消费级云计算、智能家居、智慧城市、车联网、物联网、智能制造等产生深度融合,这些都将为前端技术的发展带来新的增长和机遇。

1. WebAR & WebVR

元宇宙概念火爆全球,目前的 WebAR 和 WebVR 技术距离实现元宇宙的愿景似乎还很遥远,但借助于以 URL 的格式进行传播的优势,通过社交媒体分享形式进行推广,WebAR 和 WebVR 无疑是用户接触到元宇宙门槛的最便捷的方式,不需要购买昂贵的 VR 设备,不需要安装 APP,通过手机网页就可以进行体验。在 5G 技术逐渐普及的当今,现有的一些体验问题,例如:3D 模型体积较大,初次资源加载耗时长之类的问题也能够得到一些缓解。

那么,问题来了:前端人在这块能够做些什么?从技术上来讲,需要我们通过机器学习算法,实时地将虚拟图像覆盖到用户屏幕,并且和真实世界中的位置进行对齐,结合 WebRTC 技术实现网页浏览器实时获取和展示视频流,再利用 WebGL 的能力,进行 3D 人物模型加载,渲染和播放动画。

2. Web 3D

随着 5G 技术发展,视频加载速度会非常快,简单的实时渲染会被视频直接替代。复杂的可以通过服务器渲染,将画面传回网页中,只要传输够快,手机的性能就不再是问题。

降低 web 3D 研发成本应该是将来的一个重要发展路线,随着技术门槛的降低,会吸引更多感兴趣的人加入促使其正向发展。所以 Web 3D 可能会朝着平台化的方向发展,能提供简单高效的工具将成为核心竞争力。

3. WebRTC

WebRTC 是一项实时通讯技术,它为前端打开了信息传递的新世界大门,对于绝大多数前端开发者来说,对于信息的传递还局限于 XMLHttpRequest,升级到全双工大家会用到 WebSocket ,对于能力闭塞的前端来说,WebRTC 无疑拓宽了前端的技术门路。

九、前端未来展望

未来是留给下一代的,我们已然老了。干不动了。

1. 桌面应用 - 前端开发的下一个战场

自 2014 年 Github 推出 Electron 开源框架开始,前端跳出 Web 客户端局限,开发桌面应用的能力成为了可能,近年来,依托 Electron、React Native、Flutter 等应用框架,前端跨端开发桌面应用的概念持续升温。尽管这些方案和传统的 QT、Xaramrin 等技术栈相比,性能未必最优,但它意味着一些极具性价比的可选方案出现,大大降低了开发桌面应用的门槛。

搭上 Rust 的东风的 Tauri 受到非常多的关注,对标 Electron,主要有以下 4 点优势:

  • 包体积大小更小
  • 运行时内存占用更小
  • 安全摆在第一位
  • 真正的开源

但是理性思考,对于前端开发来说,有三个致命的缺点:

  • Tauri 使用系统 webview,会有兼容性问题,这也是 Electron 重点解决的问题
  • 抛弃了 nodejs,生态圈目前来说还是很难比得上 Electron 的
  • 底层开发要用 Rust,有一定的上手成本

但是随着 Rust 的生态起来,浏览器兼容性渐小之后,胜负犹未可知。

2. 低代码将持续成为热点话题

自从 2020 技术趋势中谈及 “低代码” 之后,低代码领域一直在持续升温。

低代码引擎的核心目标,是提供一套基础标准、设施,帮助上层平台更有效地建设。而其思路的关键,在于引擎模型及能力的完备性、以及针对不同场景下的可扩展性。

腾讯内部的低代码 Oteam 也在 21 年开始组织起来,主要的目标也是底层核心的共建。从整个行业看,低代码引擎已经开始崭露头角,且可预见到趋势还将上升。只是这个细分赛道更多可能只是大厂参与,因为其需要大量的场景支撑验证,而这是小厂或独立开发者不具备的。

之前我曾发过一篇文章,详细探讨了低代码的崛起和程序员之间的关系,有兴趣的朋友可以看个乐子:

软件吃软件,在机器学习和低代码框架的崛起中,程序员会变少吗?

3. Rust - 前端人员是时候掌握一门新语言了

随着前端生态工具的逐渐完善,大家除了探索前端的新领域之外,同时还在思考如何提高工具的性能,众所周知,JavaScript 的性能一直是被大家所诟病的点,但是前端的基础设施却是十分要求性能的,比如构建等,所以大家开始考虑是否能够用别的语言来编写前端工具,于是 Rust 吸引了大家的眼球,Rust 语言自诞生以来,就以它的安全性、性能、现代化的语法吸引了大批的开发者,在 stackoverflow 最受喜爱的编程和语言中连续多年获得榜首的位置,并且已经有众多领域都出现了 Rust 重写的项目,Linux 项目也表示正在使用 Rust 重写一部分功能,可以说 Rust 进入前端领域也是一种必然的趋势。

在前端构建领域,2021 年出现了一个十分突出的项目 —— swc,它是由 Rust 编写的构建工具,可以用来编译、压缩、打包,目前它已经被一些知名项目使用,比如 Next.js、Parcel、Deno 等,Next.js 12 直接使用了 swc 替代 babel,并在他们的官网博客表示说使用了 swc 之后,热更新速度提升到了原来的三倍,构建速度提升到了 5 倍,由此可见,Rust 性能的强大。

除了构建方面,在前端的其他领域也是有着 Rust 的身影,比如 Deno 的运行时引擎也是用的 Rust 编写的 V8 引擎;前端的下一代工具全家桶 Rome 宣布使用 Rust 重写;Node.js 可以通过 napi-rs 来调用 Rust 模块,实现高性能扩展;使用 Rust 编写的 dprint 规范代码器,要比 Prettier 快 30 倍;Rust 也可以编译成 WASM,并且出现了像 yew、percy 这样的 WASM 前端框架。

推特上,Redux 作者 Dan Abramov 在某个提问 “未来三年最值得学习的语言是什么” 下回答了 “Rust”,这或许是对前端人员的一个启发,我们也是时候学习一门新语言来让前端生态圈再次焕发活力了。

例子:现在很多的前端构建工具和项目使用Rust语言。

  • Deno(新一代的JS运行时)底层是Rust
  • Next.js(React服务端渲染框架)使用Rust编译器,能实现更快速的构建
  • Swc是一个基于rust语言开发的js编译器,利用了rust的安全无gc以及系统级语言的特性,保证了性能是接近原生开发,并且可以充分利用多核cpu。swc 对比 babel有至少 10 倍以上的性能优势

语法:

在语法上 Rust 也是极具现代化语言的特点,借鉴了函数式编程、结构化语言的特点,并且在它们的基础上也创造了许多更为先进的语法。在函数式编程的地方,也有着不少 JavaScript 的身影,比如 JS 的箭头函数对应了 Rust 的闭合函数;Rust 的数组同样也有着 map、reduce、filter 等方法;Rust 的函数也可以赋值给一个变量

在Deno的诞生发展以及于Nodejs的角逐下,Rust 可能会是 JS 基础设施的未来。

esm是什么

esm 是将 javascript 程序拆分成多个单独模块,并能按需导入的标准。和webpackbabel不同的是,esm 是 javascript 的标准功能,在浏览器端和 nodejs 中都已得到实现。使用 esm 的好处是浏览器可以最优化加载模块,比使用库更有效率

esm 标准通过importexport语法实现模块变量的导入和导出

esm 模块的特点

  1. 存在模块作用域,顶层变量都定义在该作用域,外部不可见
  2. 模块脚本自动采用严格模式
  3. 模块顶层的this关键字返回undefined
  4. esm 是编译时加载,也就是只有所有import的模块都加载完成,才会开始执行
  5. 同一个模块如果加载多次,只会执行一次

export

export语句用来导出模块中的变量

// 导出变量
export let count = 1;
export const CONST_VAR = 'CONST_VAR';
// 导出函数
export function incCount() {
    count += 1;
}
// 导出类
export class Demo {

}

function add(x) {
    return x + count;
}
// 使用export导出一组变量
export {
    count,
    add,
    // 使用as重命名导出的变量
    add as addCount,
}

// 导出default
export default add

// 合并导出其他模块的变量
export { name } from './esm_module2.js'
export * from './esm_module2.js'

import

import语句用来导入其他模块的变量

// 导入变量
import { count, incCount, CONST_VAR } from './esm_module1.js';

// 通过as重命名导入的变量
import { addCount as renamedAddCount } from './esm_module1.js';

// 导入默认
import { default as defaultAdd } from './esm_module1.js';
import add from './esm_module1.js';

// 创建模块对象
import * as module1 from './esm_module1.js';

export 导出的是值引用

esm 模块和 commonjs 模块的一个显著差异是,cjs 导出的是值得拷贝,esm 导出的是值的引用。当模块内部的值被修改时,cjs 获取不到被修改后的值,esm 可以获取到被修改后的值。

cjs 例子

// cjs_module1.js
var count = 1;
function incCount() {
    count += 1;
}

module.exports = {
    count: count,
    incCount: incCount,
}

// cjs_demo.js
var { count, incCount } = require('./cjs_module1.js');

console.log(count); // 1
incCount();
console.log(count); // 1

esm 例子

// esm_module1.js
let count = 1;
function incCount() {
    count += 1;
}

export {
    count,
    incCount,
}

// esm_demo.js
import { count, incCount } from './esm_module1.js';

console.log(count); // 1
incCount();
console.log(count); // 2

从实现原理上来看,cjs 的 module.exports是一个对象,在运行期注入模块。在导出语句module.exports.count = count执行时,是给这个对象分配一个count的键,并赋值为1。 这之后模块中的count变量再怎么变化,都不会干扰到module.exports.count

esm 中的export { count }是导出了count变量的一个只读引用,等于说使用者读取count时,值的指向还是模块中count变量的值。

可以看阮一峰的这篇文章

在 html 中使用 esm

使用script标签引入 esm 文件,同时设置type=module,标识这个模块为顶级模块。浏览器将 esm 文件视为模块文件,识别模块的import语句并加载。

<script src="./esm_main.js" type="module"></script>

如果不设置type=module,浏览器认为该文件为普通脚本。检查到文件中存在import语句时,会报如下错误

esm的加载机制

esm 标准没有规定模块的加载细节,将这些留给具体环境实现。大致上分为下面四个步骤:

解析:实现读取模块的源代码并检查语法错误

加载:递归加载所有import的模块

链接:对每个加载的模块,都生成一个模块作用域,该模块下的所有全局声明都绑定到该作用域上,包括从其他模块导入的内容

运行时:完成所有import的加载和链接,脚本运行每个已经加载的模块中的语句。当运行到全局声明时,什么也不会做(在链接阶段已经将声明绑定到模块作用域)。

可以看下 mdn 上的这篇深入 esm 的文章

动态加载模块

esm 的一个重要特性是编译时加载,这有利于引擎的静态分析。加载的过程会先于代码的执行。却也导致import导入语句不能在函数或者if语句中执行

// 报语法错误
if (true) {
    import add from './esm_module1.js';
}

es2020 提案引入import()函数,用来动态加载模块,并且可以用在函数和if语句中。

import('./esm_module1.js')
  .then(module => {
    console.log(module);
  })

import()函数接受要加载的模块相对路径,返回一个Promise对象,内容是要加载的模块对象。

使用import()函数还可以实现根据变量动态加载模块

async function getTemplate(templateName) {
	let template = await import(`./templates/${templateName}`);
	console.log(template);
}

getTemplate("foo");
getTemplate("bar");
getTemplate("baz");

ESM与CommonJS模块化区别

CommonJS语法

// 导出
module.exports = {}
module.exports.a = xxx
// 导入
var xxx = require('xxx');

ESM语法

// 导出
export const aaa = xxx
export bbb
export default xxx

// 导入
import aaa from './xxx.js'
import {a,b} from './xxx.js'
import * as aaa from './xxx.js'
import { default as defaultAdd } from './esm_module1.js';

两者区别

1.CommonJS 模块输出的是一个值的拷贝,ESM模块输出的是值的引用

当模块内部的值被修改时,cjs 获取不到被修改后的值,而ESM可以。

2.CommonJS 模块是运行时加载,ESM 模块是编译时加载

因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。

而 ESM不是对象,它的对外接口只是一种静态定义,一个“符号连接”,在代码静态解析阶段就会生成。必须写道模块顶部。

es2020 提案引入import()函数,可用来动态加载模块。

3.CommonJS 是同步加载模块,ESM是异步加载模块

CJS 由于运行机制是值拷贝,require会阻塞后续代码。值拷贝完成会缓存其结果。

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。也就是说模块不会缓存运行结果,而是动态地去被加载的模块取值。

4.ESM 引用的模块变量是只读的

由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

5.ESM 同一个模块如果加载多次,只会执行一次

1.vue-router导航守卫(生命周期钩子)

导航守卫主要⽤来对路由的跳转进⾏监控,控制它的跳转或取消,路由守卫有全局的, 单个路由独享的, 或者组件级的。

导航钩⼦有3个参数

  • to:即将要进⼊的⽬标路由对象;
  • from:当前导航即将要离开的路由对象;
  • next:调⽤该⽅法后,才能进⼊下⼀个钩⼦函数(afterEach)。

具体有哪些钩子

  1. 全局前置守卫:router.beforeEach
  2. 全局解析守卫:router.beforeResolve
  3. 全局后置钩子:router.afterEach
  4. 路由独享钩子:在路由配置中添加beforeEnter钩子
  5. 组件内钩子:beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

简单理解 beforeEach

每次通过跳转路由时,都会触发beforeEach这个钩⼦函数,这个回调函数共有三个参数,to,from,next这三个参数,to表⽰我要跳转的⽬标路由对应的参数,from表⽰来⾃那个路由,就是当前导航即将要离开的路由对象,next是⼀个回调函数,⼀定要调⽤next⽅法来resolve这个钩⼦函数,否则无法进⼊下⼀个钩⼦函数,也无法加载路由。

详见文档

2.导航守卫解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

3.路由懒加载如何实现,原理是什么?

路由懒加载需要导入异步组件,最常用的是通过import()来实现它。

function load(component) {
    return () => import(`views/${component}`)
}
// 箭头函数
const asyncPage = () => import('./views/home.vue')
// import()函数需要作为返回值,其返回Promise

编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由时(访问该路由时)才会去按需加载对应的路由组件js文件。可以加快项⽬的加载速度。

在Vue3中,在组件中使用异步组件时需要使用defineAsyncComponent()函数才能实现懒加载,但在vue-router中同样可以使用import()方法

这时由于在 Vue 3 中,函数组件被定义为纯函数,异步组件定义需要通过将其包装在一个新的 defineAsyncComponent helper 中来显式定义。

import { defineAsyncComponent } from 'vue'
const home = defineAsyncComponent(() => import('@/views/home.vue'))
export default {
  name: 'async-components',
  components:{
    home
  }
};

4.vue-router如何做用户登录权限?

第一次打开页面或刷新页面时候可以在路由前置钩子router.beforeEach中查询用户信息。再根据返回的用户信息和to,from参数判断登录权限(这里要把用户的信息和登录状态做一个缓存),然后使用next回调控制路由跳转方向。具体的代码要根据具体的项目进行实施。

不是第一次打开页面情况,跳转时直接判断缓存中的登录状态和用户信息来判断是否拥有权限。若有权限则继续跳转到指定的路由。

5. vue-router 3.1.0 <router-link>新增的v-slot属性怎么⽤?

router-link 通过⼀个作⽤域插槽暴露底层的定制能⼒。方便我们更自由的定制导航链接的形式。记得把 custom 配置传递给 <router-link>,以防止它将内容包裹在 <a> 元素内。

详见文档

6. <router-view> 的 v-slot

详见文档

7. 如何实现⼀个路径渲染多个组件?

可以通过命名视图(router-view),它容许同⼀界⾯中拥有多个单独命名的视图,⽽不是只有⼀个单独的出⼝。如果router-view 没有设置名字,那么默认为default

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。在配置routes时,确保正确使用 components 配置 (带上 s):

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],
})

详见文档

8. 如何实现多个路径共享⼀个组件?

只需将多个路径的component字段的值设置为同⼀个组件即可。

9. 如何监测动态路由的变化

可以通过watch⽅法来对$route进⾏监听,或者通过导航守卫的钩⼦函数beforeRouteUpdate来监听它的变化。

在Vue3中可使用useRoute()onBeforeRouteUpdate

10. Vue 如何去除url中的 #

哈希字符(#)部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。

如果你担心这个问题,可以使用 HTML5 模式,将路由模式改为history

路由切换时本质是向history栈中添加一个路由,也就是添加一个history记录。

vue2(vue-router 3.x,已经弃用):

const router = new VueRouter({
  mode: 'history', // 也可设为 hash
  routes: [...]
})

vue3(vue-router 4.x):

import { createRouter, createWebHashHistory } from 'vue-router'

// hash 模式是用 createWebHashHistory() 创建的:
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
})

// 用 createWebHistory() 创建 HTML5 模式,推荐使用这个模式:
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
})

11. $route 和 $router 的区别

$route是用来获取路由信息的:

它是当前路由信息的⼀个对象,⾥⾯包含路由的⼀些基本属性,包括name(路由名称)、meta(配置的路由元信息)、path(当前路径)、hash(hash参数)、query(查询参数)、params(当前传递参数)、fullPath(当前全路径)、matched(匹配项)等等。

每一个路由都会有一个$route对象,是一个局部的对象。

$router是用来操作路由的:

$router是VueRouter的一个实例,他包含了所有的路由,以及路由的跳转方法,钩子函数等,也包含一些子对象(例如history)。

12. Vue-router 使⽤params与query传参有什么区别

⽤法上

query要⽤path来引⼊,params要⽤name来引⼊,接收参数都是类似的,分别是this.$route.query.namethis.$route.params.name

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// `params` 不能与 `path` 一起使用,这时,params无法传递
router.push({ path: '/user', params: { username } }) // -> /user

// 占位符传参时,params参数对应的字段会被拼接到path中
// 使用 `name` 和 `params` 从自动 URL 编码中获益
const routes = [
  { name: 'user',path: '/user/:username' },
]
router.push({ name: 'user', params: { username } }) // -> /user/eduardo

展⽰上

  • query更加类似于我们ajax中get传参,params则类似于post,说的再简单⼀点,前者在浏览器地址栏中显⽰参数,后者则不显⽰
  • params是路由的⼀部分,必须要有,组件才能正常执行。query是拼接在url后⾯的参数,若没有也可正常执行组件。
  • query不设置也可以通过浏览器url进行传参,params不设置的时候,刷新页⾯或者返回参数会丢失
  • 刷新页面后,通过params传参会出现参数丢失的情况,可以通过query的传参⽅式或者在路由匹配规则加⼊占位符即可以解决参数丢失的情况。

13. Vue路由实现的底层原理

Vue的路由实现:hash 模式和 history 模式

hash模式:

早期前端路由的实现是基于window.location.hash 来实现的,window.location.hash 的值就是 URL中#后面的内容。

特点:hash虽然在URL中,但不被包括在HTTP请求中;只是用来指导浏览器动作,对服务端无作用,且hash的切换不会重加载页面。

hash 模式下,仅 # 号之前的内容会被包含在请求中,因此对于后端来说,即使路由切换错误,也不会返回 404 。

但是 hash 模式有两个缺点:

  1. url 不太美观
  2. 对SEO不太友好

history模式:

history采用HTML5的新特性;且提供了两个新方法:history.pushState()history.repalceState()可以对浏览器历史记录栈内存进行修改,且页面不会重载。

history.pushState方法接受三个参数,依次为:

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,我们使用pushState方法在浏览记录(history对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

这样地址栏就可以看到我们自己存的标识了。历史记录也就存进去了。

每当激活的历史记录发生变化时,都会触发popstate事件,调用history.pushState()或者history.replaceState()不会触发popstate事件。pushState事件只会在其他浏览器操作时触发, 比如点击后退按钮(或者在JavaScript中调用history.back()history.go()方法)。

window.addEventListener('popstate', function(event) {
  //做一些操作
  console.log(event.state) // history.pushState()中传入的state状态对象
});

缺点:若没有对服务器(如nginx)进行配置,刷新本不存在的(pushState新增的历史记录)url页面时会出现404状况。

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )

微任务主要包含PromiseMutationObserver(变动观察器)是监视DOM变动的接口)、process.nextTick(Node.js )

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

//-------------------------------------------

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

1.eval(jsstr)和new Function(jsstr)

evalnew Function都可以动态解析和执行字符串。会将字符串转义为js代码。区别如下:

1.对解析内容的运行环境判定不同:eval中的代码执行时的作用域为当前作用域,它可以访问到函数中的局部变量,也可以访问全局变量。new Function中的代码执行时的作用域为全局作用域,不论它的在哪个地方调用的,所以它访问的都是全局变量,无法访问局部作用域的变量。

var a = 'global scope' 
function b(){ 
    var a = 'local scope' 
    eval('console.log(a)') //local scope 
    (new Function('','console.log(a)'))() //global scope
}
b()

2.eval() 安全性较低,最好不要使用。它使用与调用者相同的权限执行代码。 eval() 运行的字符串代码若被恶意方修改,将会影响计算机的安全。

3.evalnew Function更慢,因为它必须调用 JS 解释器,很吃性能。

1.js判断变量类型

  1. typeof可以判断一般类型。但无法准确识别对象。所有对象或类对象类型(null)都为"object",比如数组typeof [] == "object"。JS数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
  2. typeof 与其他条件组合判断:typeof arr=="object" && !!arr.push
  3. instanceof可以判断具体对象类型。用来判断对象是否为某个构造函数的实例。[] instanceof Array==true
  4. Object.prototype.toString.call()可以判断任意变量类型。Object.prototype.toString.call([])=="[object Array]"
  5. .constructor.name可以判断变量类型(nullundefined除外)。true.constructor.name=="Boolean"
  6. 判断数组也可以用Array.isArray(arr)方法

2.数组常用方法

  1. pop()尾部删除,返回尾部元素,会改变原数组
  2. push()尾部插入,会改变原数组
  3. unshift()头部插入,会改变原数组
  4. shift()头部删除 ,这两个别搞混。返回头部元素,会改变原数组
  5. join()把数组元素放入字符串,使用指定的分隔符进行分割
  6. reverse()倒序数组元素顺序,返回倒序后的新数组。会改变原数组
  7. splice()删除元素,并向数组添加新元素。会改变原数组
  8. sort()对数组对象进行排序—字符串排序按每一个英文字母先后/数字排序按字符串处理(实际上是每一位的排序),会改变原数组
  9. slice()从开始和结束位置截取数组中的元素返回新数组
  10. concat()连接(合并)两个数组
  11. arr.length=0 清空数组
  12. map()/find()/filter()/reduce()/forEach()/some()/every():ES6数组新方法。(every:一假即假,some:一真即真)

3.数组去重

  1. indexOf/includes循环去重:申明一个新新数组,判断新数组中是否包含当前项,若没有,则push
  2. ES6 Set对象去重:Array.from(new Set(arr))
  3. Object键值对去重:把数组的值存为key,判断obj[arr[i]]是否存在,若存在则重复。
  4. 利用ES6的数组reduce方法(第二个参数[]为初始值):arr.reduce((unique,item) =>unique.includes(item)?unique:[...unique,item], []);

4.去除字符串首尾空格

  1. str.trim()
  2. 正则:str.replace(/(^\s*)|(\s*$)/g,"")

5.requestAnimationFrame

requestAnimationFrame()方法用于代替使用定时器开发的动画。此方法的回调函数会在浏览器重绘之前调用。此方法的执行频率与显示器的刷新频率相关。回调函数执行次数通常是每秒60次,但在大多数浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的<iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

大部分显示器的刷新频率为每秒60次,也就是每次刷新间隔为16.7ms,我们在渲染动画时只有每一帧间隔时间<=16.7ms才能让用户觉得不卡顿。

6.引用类型常见有哪些对象

Object、Array、RegExp、Date、Function、Math、String、Number、Boolean

7.对象深拷贝、浅拷贝

对象引用:只引用对象,没有真正的复制。只复制引用(内存地址指向/栈内存),没有复制真正的值(堆内存),对其进行修改会影响原对象。

浅拷贝:只复制一层对象的属性。如果对象还有嵌套对象,则无法复制。如Object.assign()

深拷贝:复制了对象真正的值。对其任何操作都不会影响被拷贝的对象。

实现浅拷贝:

  1. 扩展运算符
  2. Object.assign
  3. 数组通过slice()的方法
let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

//-------------------------------------------

let a = {
    age: 1
}
let c = {...a}
c.age = 2
console.log(a.age) // 1

实现深拷贝:

  1. 递归浅拷贝(可行)。
  2. 深度遍历对象,递归嵌套对象,将其键值对赋值给另外一个新创建的对象,但性能堪忧。
  3. 使用JSON反序列化和序列化,性能最快JSON.parse(JSON.stringify(oldObj))(缺点:只能深拷贝对象和数组,会忽略undefined和symbol,不能序列化函数,不能序列化循环引用的对象)
  4. 第三方库如 jQuery.extend
  5. ProxyObject.defineProperty来拦截 set 和 get 就能阻止拷贝对象修改原对象(性能高,无缺点),深拷贝点此参考

注意:数组的slice方法和concat方法无法进行深拷贝,只能浅拷贝。因为嵌套对象或数组会被引用。对象的Object.assign()方法同样如此。

8.判断两个对象是否相等

"a==b"或者Object.is(a,b)可以判断两个对象是否相等,但仅限于两个对象引用相同,是同一个对象。深浅拷贝对象无法判断。

若要判断两个不同引用的对象是否相等,得通过深度遍历及递归方法(递归时判断值是否是Object)进行值的判断(遍历及递归之前先通过Object.getOwnPropertyNames拿到两个对象的所有键名,比较键名是否相等)。

这时候不能用JSON.stringify()方法去比对对象,因为序列化之后,键是有序的,无法比较。

9.js变量类型

基本类型有六种nullundefinedbooleannumberstringsymbol

其中 JS 的数字类型是浮点类型的,没有整型。NaN 也属于 number 类型,并且 NaN 不等于自身。对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。

typeof 对于基本类型,除了 null 是object,其他都可以显示正确的类型。(在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object

引用类型:objectfunctionarraymapset

引用类型在使用过程中会遇到浅拷贝和深拷贝的问题。typeof 对于引用类型,除了函数都会显示 object

10.隐式类型转换

使用if条件判断或使用==/&&/||等运算符时会对变量进行隐式类型转换。

隐式类型转换时,除了 undefined, null, false, NaN, '', 0, -0[],其他所有值都转为 true,包括所有对象(这里注意空数组会被转为false)。

对象在隐式类型转换时,首先会调用其原型上的 valueOf 方法或 toString方法。valueOf 优先于toString。并且这两个方法你是可以重写的。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
}
1 + a // => 1
'1' + a // => '10'

//-------------------

let a = {
  toString() {
    return '1';
  },
}
1 + a // => '11'
'1' + a // =>  '11'

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

对于加号需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1

console.log(+'1') // 1
console.log(-'1') // -1
console.log(+'a') // NaN
console.log(-'a') // NaN

console.log(+'') // 0
console.log(+[]) // 0
console.log(+true) // 1
console.log(+false) // 0
console.log(-true) // -1
console.log(-false) // -0
console.log(-false) // -0
console.log(-[]) // -0
console.log(-'') // -0
console.log(+[123]) // 123
console.log(+['123']) // 123
console.log(+[true]) // NaN
console.log(+[123,1]) // NaN

// 'a' + + 'b'  等同于 'a' + (+'b') 等同于  'a' + NaN
console.log('a' + (+'b'))  // "aNaN"
console.log( 'a' + NaN)  // "aNaN"

空数组会被转为false

[] == ![] // -> true
[]==false //true

11.++运算

++在前,返回值是新值

++在后,返回值是旧值

var a = -1
if(++a){ // 这里++a的值为新值0
  console.log(666)
}else{
  console.log(888)
}
// 结果为888

// ---------------------------------------

var a = -1
if(a++){ // 这里a++的值为旧值-1
  console.log(666)
}else{
  console.log(888)
}
// 结果为666

作为前端开发中现行最火的框架之一,基于此,总结了一些 Vue 方面经常出现的问题,留给自己查看消化,也分享给有需要的小伙伴。

由于篇幅较长,不能将所有知识点放到一篇文章内。这是Vue重点知识梳理理论篇的第二篇。前端茫茫,学无止境。

1.模板引擎原理(指令和插槽表达式如何生效)

使用with改变作用域,渲染数据。并将其包到字符串中。`var render = with(vm){return ${data}}`,使用new Function(render)执行字符串语句。

2.描述组件渲染和更新过程

渲染组件时,会通过Vue.extend()方法构建子组件的构造函数并实例化为vueComponent。extend方法会合并一些父类如Vue的属性。最终手动调用$mount()进行挂载。更新组件时会进行patchVnode流程,也就是diff流程。

3.为什么要使用异步组件加载方式?

如果组件功能较多,打包出的文件会过大。导致页面加载过慢。这时候可以使用import("url")函数异步加载组件的方式,可以实现文件的分隔加载。

  1. 会将组件分开打包。减小体积。
  2. 会采用jsonp的方式加载,有效解决一个文件过大问题。
  3. import语法是webpack提供的。
  4. 异步组件一定是一个函数。
components:{
    mycomp:()=>import("../components/mycomp.vue")
}

4. Vue-loader 是什么

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。本质上,webpack loader 将所有类型的文件,转换为应用程序可以直接引用的模块。所以 loader 就是个搞预处理工作的。

Vue-loader 可以解析和转换 .vue ⽂件,提取出其中的逻辑代码 script 、样式代码 style 、以及模版 template ,再分别把它们交给对应的 Loader 去处理。

5.Vue 中 key 的作用

key主要作用是为了高效得更新虚拟DOM。原理是VUE在patch过程中会通过key精准判断两个节点是否是同一个。从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

准确: 如果不加key,那么vue会选择复⽤节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产⽣⼀系列的bug.。

快速: key的唯⼀性可以被Map数据结构充分利⽤,相⽐于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1)

  1. 若不设置key,会在列表的渲染过程中发生一些隐蔽的bug,比如新增数据乱序。
  2. 重复的 key 会造成渲染错误。
  3. key是为Vue中的vnode标记的唯⼀id,通过这个key,我们的diff操作可以更准确、更快速
  4. diff算法的过程中,先会进⾏新旧节点的⾸尾交叉对⽐,当⽆法匹配的时候会⽤新节点的key与旧节点进⾏⽐对,然后超出差异

没有key情况还会导致节点的删除和重建,影响性能。若有key,在进行更新时会直接保留原有节点,只插入新节点,无需删除重建。

  1. v-for循环的时候,key属性只能使用number/string
  2. key属性值必须是唯一标识符。
  3. key属性不要使用索引作为标识。

用引索index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

1.列举ES6的一些新特性

  1. 默认参数
  2. 模板字符串
  3. 解构赋值
  4. 增强的对象字面量
  5.  箭头函数
  6. Promises 异步
  7. generator和async/await
  8. 块作用域 和let和const
  9. Class 类
  10. Modules 模块

2.let ,const,var及其区别

JS代码在执行前会进行预解析。预解析会进行变量提升。

var 声明的变量会发生提升(提升到当前作用域顶部)。虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。

注意:是申明的提升,赋值并不会提升。赋值和申明并不同时发生。只有先声明才能使用这个变量,后申明的值前面无法直接使用。

(这里注意undefined的写法,避免手写代码时写错)

console.log(a) // undefined,如果后面没有申明变量a,这里就不是undefined而是直接报错:is not defined
var a = 1
// 同理如下
var a1 = a2+1
var a2 = 10
console.log(a1) // NaN
//----------------------------------------
var a = 10
var a
console.log(a) // 10
//原因:var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。
var a = 'hello world';
var a = undefined;
console.log(a) //undefined
// var a = undefined虽然作用等同于var a 但由于赋值的覆盖,所以会打印undefined
//----------------------------------------
console.log(a) // ƒ a() {} 这就是为什么函数可以放到下面,在上面依然能调用的原因
function a() {}
var a = 1 // 函数会被提升,且优先于变量
//-----------------------------------------
let a = 1
let a = 2 //error:Identifier 'a' has already been declared
// let 无法重复声明变量

let和const 声明的变量不会提升。

  1. let 和 const 在全局作用域下声明变量,变量并不会被挂载到 window 上,而 var 会被挂载到window对象中。
  2. let 和 const 在声明 变量 之前如果使用了这个变量,就会出现报错,是因为存在暂时性死区。(let实际也会进行提升,只不过因为暂时性死区导致了并不能在声明前使用。)
  3. let和const无法重复声明,而var可以重复申明,且var后声明的值会覆盖前值(同名函数也会覆盖之前的函数)。如果后申明的重复变量未赋值,会被忽略。
  4. var没有块级作用域,只有函数作用域。而let和const有块级作用域
  5. const申明了变量后必须赋值。不赋值会报错。(const a;是错误的)
  6. const定义的常量不能被修改。
console.log(a) // Uncaught ReferenceError: a is not defined
let a // 这里将提升限制了,所以提示a未申明

函数作用域:

function fn(){
   var aaa = 1
}
fn()
console.log(aaa) // 报错,未定义

经典面试题:

var a = 1;
fn();
function fn(){
    console.log(a); // undefined
    var a = 2
    console.log(a) // 2
}

// 第一个打印 undefined 是由于
// 函数申明也会被提升,且优先于变量
// 函数作用域中的变量的申明也会提升到函数最顶部
// 函数作用域中变量为局部变量,作用域链优先取局部值,局部没有才会找外层
// 上述代码等同于下:

function fn(){
    var a
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
var a = 1;

面试题2

function fn(){
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
fn() // 在这里执行时候,a只是提升申明,还没有赋值
var a = 1;

//----------------------------

function fn(){
    console.log(a); // 1
    a = 2
    console.log(a) // 2
}
var a = 1;
fn() // 在这里执行,a已经被赋值

块级作用域:

js有全局作用域、函数作用域。在ES6之后新增块级作用域。也就是`{}`中的作用域。if和for语句是典型的块级作用域。var在函数作用域中不会垮函数访问,而可以垮块级访问。也就是说var只承认函数作用域。
for(var i=0;i<3;i++){
    console.log("a:",i)
}
console.log("b:",i)
// a: 0
// a: 1
// a: 2
// b: 3

for(let j=0;j<3;j++){
    console.log("a:",j)
}
console.log("b:",j)
// a: 0
// a: 1
// a: 2
// j is not defined

当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到当前的函数(只读),而不会指向全局的变量。但在全局中,访问不到该函数。

var foo = 1
(function foo() {
    foo = 10
    console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }

console.log(foo) //1

小结:

函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部,而申明的值不会提升

var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用

var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会

let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

var只能提升变量或函数的申明,无法提升申明时的赋值。

var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。但是赋值还是继续有效。

更多容易出错的题目:

关于变量提升和作用域相关的题目

3.为什么要使用模块化?都有哪几种方式,有什么特点?

前端模块化时将复杂的js文件分为多个独立的模块。用于重用和可维护。这样会引来模块之间相互依赖的问题。所以有了commonJS规范,AMD,CMD等规范,以及打包工具webpack,gulp等。

使用模块化可以给我们带来以下好处

  1. 解决命名冲突和作用域污染
  2. 提供可复用性
  3. 提高代码可维护性

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污 染全局作用域的问题。

早期的模块化是AMD 和 CMD技术。但现在基本不用了。像以前的requireJS 是AMD方 法。Sea.js是CMD技术。

AMD,异步模块定义(Asynchronous Module Definition),它是依赖前置 (依赖必须一开始就写好)会先尽早地执行(依赖)模块 。换句话说,所有的require都被提前执行(require 可以是全局或局部 )。

AMD规范只定义了一个函数 define,它是全局变量。用法:

defind(id, dependencies, factory)

CMD(Common Module Definition)更贴近 CommonJS Modules/1.1 和 Node Modules 规范,一个模块就是一个文件;它推崇依赖就近,想什么时候 require就什么时候加载,实现了懒加载(延迟执行 ) ;它也没有全局 require, 每个API都简单纯粹 。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

define(function(require, exports, module) {
    // 模块代码
});

AMD与CMD的比较

  • AMD:依赖前置,预执行(异步加载:依赖先执行)。CMD:依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething();
    // 省略1万行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething();
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // 省略1万行
    b.doSomething();
})

CommonJS 最早是 Node 在使用,目前也仍然广泛使用。基本语法:

module.exports = {} // 导出全部
exports.a = {} // 部分导出
var a = require(“./a.js”) // 引入

ES Module 是最新的原生实现的模块化方式。与 CommonJS 有以下几个区别:

CommonJS 支持动态路径导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案。

CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响。

CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。

ES Module 会编译成 require/exports 来执行的。

// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function()

4.Proxy 可以实现什么功能

Vue3.0 通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式(双向绑定)。

let p = new Proxy(target, handler) //target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

与defineProperty 区别:

  1. Proxy 无需递归为每个属性添加代理,一次即可完成所有父子元素的监听,性能更好
  2. 在vue中Object.defineProperty有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,缺陷是浏览器兼容性不好。

优势:

  • 可以劫持整个对象
  • 可以监听对象动态属性的添加
  • 可以监听到数组的变化,包括数组的索引和数组length属性
  • 可以监听删除属性
  • 操作时不是对原对象操作,new Proxy 返回⼀个新对象(它不直接操作对象,而是代理模式,通过对象的代理对象进行操作)。

5.map, filter, reduce 各自有什么作用?

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的 数组中。map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组。

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组。 作用:过滤出需要的元素。filter 的回调函数也接受三个参数,用处也相同。

reduce 可以将数组中的元素通过回调函数最终转换为一个值。如计算数组元素的和:

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)

reduce 接受两个参数,分别是回调函数和初始值。

首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入

回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数

在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入

所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6

6.箭头函数和普通函数区别

语法不同:语法简洁,箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

参数写法不同:

① 如果箭头函数没有参数,直接写一个空括号即可。
② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。
③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

函数体不同:

①如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }和return关键字。

let f = val => val;// 等同于let f = function (val) { return val };
let sum = (num1, num2) => num1 + num2;// 等同于let sum = function(num1, num2) {
return num1 + num2;};

②如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });

③如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字,代表无返回值。

let fn = () => void doesNotReturn();

箭头函数最常见的用处就是简化回调函数。

[1,2,3].map(x => x * x);// map箭头函数写法
var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);// 排序箭头函数写法

箭头函数不会创建自己的this

没有自己的this,它只会从自己的作用域链的上一层继承this。它会捕获自己在定义时(注 意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数 中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

所以在使用时,普通函数需要用到外部的this时要将this赋值给另一个变量传进去,而箭头 函数不需要。直接可以再函数内部使用外部的this。常见使用场景为vue的方法。

普通函数简单调用时this指向函数本身。注意是在调用时才会确定this指向。

普通函数作为对象的方法调用时,this指向它所属的对象。

var id = 'GLOBAL';var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'

箭头函数继承而来的this指向永远不变,永远是定义时外层环境this

.call()/.apply()/.bind()无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this 定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽 然这么做代码不会报错。

由于this指向不是自身,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错。

箭头函数没有自己的arguments,在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。可以在箭头函数中使用rest参数(三点运算符)代替arguments对象。

Var a = (...values)=> {
  console.log(values[0]) // 2
  console.log(values[1]) // 5
  console.log(values[2]) // 8
}


箭头函数不会创建this,不能作为构造函数,不能使用new关键字。另外箭头函数没有原型prototype

1.js事件循环机制

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )、requestAnimationFrame

宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval

微任务主要包含PromiseMutaionObserver(IOS)、process.nextTick(Node.js )

微任务的优先级: process.nextTick > Promise > MutationObserver

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

//-------------------------------------------

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

2.setTimeout(fn,100)中的100毫秒时如何权衡的

当我们执行到setTimeout定时器代码时候,会把代码放到一个定时触发器线程,然后开始计时。计时结束后,会将定时的回调函数插入任务队列。必须等到主线程中的代码及先进入队列的任务执行完,主线程才会去执行这个回调。所以有可能要等很久,所以没办法保障回调函数一定会来100毫秒后执行。

这里注意,并不是等主线程执行完毕才开始计时,而是执行到定时器代码块时候就开始计时了。影响回调函数执行时间的是主线程的执行时间。

定时触发器线程

浏览器定时计数器并不是由JS引擎计数的,,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确。因此通过单独线程来计时并触发定时是更为合理的方案。

3.js基本数据类型和引用数据类型的区别

  1. js基本数据类型有:Number、String 、Boolean、Null和Undefined;引用类型有:Object 、Array 、Function。
  2. 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,当这个变量重新赋值之后看起来变量的值是变了,但这里实际只是变量名指向变量的一个指针变了(改变的是指针的指向)。该变量是不会变的。引用类型时可以被改变的。一旦重新赋值,其值则被改变。
  3. 基本数据类型不可以添加属性和方法,但是引用类型可以。
  4. 基本数据类型的赋值都是简单赋值,如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新的值。然后把该值复制到为新变量分配的内存位置上。而引用类型的赋值是对象的引用。
  5. 基本数据类型的比较是值的比较,引用类型的比较是引用的比较。引用数据了类型比较的是内存地址是否相同。
  6. 基本数据类型是存放在栈区的,引用数据类型会同时保存在栈区和堆区。栈区保存的是一个地址。

4.一个单机网页游戏老是卡顿或崩溃,原因可能是什么

  1. 内存溢出问题:及时清理内存,将无用对象回收(垃圾回收机制)
  2. 资源文件过大:选择较小的图片或压缩图片
  3. 资源加载过慢:预加载资源,在游戏初始化之前就加载资源
  4. 动画绘制频率问题:每一帧的绘制间隔时间与显示器刷新频率保持一致。
  5. 单机游戏与网络无关。如果是网络游戏,ping值过高,请确认自己网络是否延迟过高。

5.函数式编程思想

定义:函数式编程(FP)是一种编程范式(其针对与面向对象编程范式OOP,函数式与面向对象冲突)。函数式编程是把运算过程尽量写成一系列嵌套的函数调用。 这里的函数式指的是数学函数,而非编程语言中的函数。

纯函数:函数式编程允许我们编写的函数是纯函数。纯函数的特点:

  1. 如果给定相同的参数,则返回相同的结果(也称为确定性)。可以理解为返回结果不因除参数外的任何变量的改变而改变。
  2. 它不会引起任何副作用。比如修改全局对象或修改通过引用传递的参数。

函数式编程的特点:

  1. 数据和行为分离(OOP是把数据和行为打包)
  2. 不可修改性(immutability。OOP以及面向过程编程很多时候都会对内存状态进行修改,比如给变量或成员重新赋值、往一个集合里添加/删除数据等,FP不允许这么干)
  3. 一等公民是函数(函数可以赋值给变量,可以当参数传递,函数的返回值可以是函数。OOP里的一等公民只有对象)
  4. 闭包

函数式编程的优势:

  1. 并行:纯函数无需任何修改即可并行执行。
  2. 无副作用:不会修改全局变量和引用变量,不会干扰其他程序。
  3. 更容易测试:函数式程序的 bug 不依赖于与任何与其无关的代码,你遇到的问题就总是可以再现。我们可以使用不同的上下文对纯函数进行单元测试。
  4. 引用透明性:如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
  5. 不可变性:只要参数不变,返回值永远不变
  6. 高内聚低耦合:函数是一阶公民。函数可以作为参数或返回值。我们可以组合不同的函数来创建具有新行为的新函数。

6.JS函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

Currying有哪些好处

① 提高复用率

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

② 提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

// 提前确认调用哪个方法,不用每次调用都判断
var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

③ 延迟运行

Function.prototype.bind = function (context) {
    var _this = this,args = Array.from(arguments).slice(1)
    return function() {
        return _this.apply(context, args)
    }
}

7.立即执行函数的作用

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 立即执行函数执行完后就立刻被销毁,避免常驻内存。(所以命名的立即执行函数外部无法访问,实际情况是已被销毁)
(function foo() {
    console.log(123);
})();
//已被销毁,所以访问不到。所以一般用匿名函数
foo();  // ReferenceError: foo is not defined

// 要访问自执行函数的执行结果
var num = (function (a, b) {
    return a + b;
})(1, 2);

//其实这种写法不用括号把函数包起来也是可以的
var num = function (a, b) {
    return a + b;
}(1, 2);
//在执行赋值操作的时候,上面的代码已经变成了一个表达式,而 JavaScript 解释器认为 函数表达式 是可以直接被执行的。

// 如果要再次访问自执行函数,需要将其作为返回值
// 这时就不能用匿名函数了
var num = (function aaa(a, b) {
    console.log(a+b)
    return aaa
})(1, 2);
num(3,4)
num(3,4)(5,6)

// 利用arguments.callee方法返回当前函数用于访问自执行函数
// 这样免得给匿名函数命名
// 但arguments相关的方法在严格模式下会报错
var num = (function(a, b) {
    console.log(a+b)
    return arguments.callee
})(1, 2);

使用场景:

  1. 立即执行函数执行完后就立刻被销毁,所以如果函数只被执行一次的时候就可以将其换成立即执行函数。
  2. 创建独立作用域,避免变量干扰。
  3. 用于初始化方法(数据初始化)。可避免内部变量污染全局。
  4. 可以用来封装一些插件、轮子、方法。

ES6中,块级作用域已经可以取代立即执行函数。可以将需要立即执行的代码放到花括号{}中。

8.定时器的最小间隔(延迟)时间

HTML5标准规定

  • setTimeout的最短时间间隔是4毫秒;
  • setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒