当前位置:   article > 正文

20212022最新Web前端经典面试试题及答案-史上最全前端面试题(含答案)、前端面试题大全、前端进阶必知必会知识点_123saas平台(在线教育全生态北京总部)

123saas平台(在线教育全生态北京总部)

  推荐文章:

vue3面试题:最新vue3.0前端经典面试试题及答案(持续更新中……)_南北极之间的博客-CSDN博客打包大小减少41%初次渲染快55%, 更新渲染快133%内存减少54%......使用Proxy代替defineProperty实现响应式重写虚拟DOM的实现和Tree-Shaking......Vue3可以更好的支持TypeScriptComposition API(组合API)setup配置ref与reactivewatch与watchEffectprovide与inject......新的内置组件FragmentTeleportSuspense其他改变新的生命周期钩子data 选项应始终被声明为一个函https://blog.csdn.net/qq_22182989/article/details/125781704

VUE之VUEX常见面试题大全汇总--史上最全【vuex面试题】_勤动手多动脑少说多做厚积薄发-CSDN博客官方文档对vuex不太熟悉的小伙伴。可以先去看看vuex官方的文档:提示这是与 Vue 3 匹配的 Vuex 4 的文档。Vuex 是什么? | Vuexhttps://vuex.vuejs.org/zh/如果您在找与 Vue 2 匹配的 Vuex 3 的文档,请在这里查看。Vuex 是什么? | Vuexhttps://v3.vuex.vuejs.org/zh/下面是常见面试题:1.vuex是什么?怎么使用?哪种功能场景使用它?Vuex 是一个专为 Vue..https://blog.csdn.net/qq_22182989/article/details/122854506

 JavaScript数组的常用方法总结:遍历,复制,反转,排序,添加,删除(前端常见面试题必考必问_南北极之间-CSDN博客1. Array.push()向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。一个:var fruits = ["Banana", "Orange", "Apple", "Mango"];var x=fruits.push("Kiwi");console.log(fruits)//(5) ["Banana", "Orange", "Apple", "Mango", "Kiwi"] console.log(x)//5 x是 新的数组长度多个:var fruits =https://blog.csdn.net/qq_22182989/article/details/120995392

2021前端开发最全vuejs面试题(持续更新)_勤动手多动脑少说多做厚积薄发-CSDN博客1、什么是MVVM?答:MVVM是是Model-View-ViewModel的缩写,Model代表数据模型,定义数据操作的业务逻辑,View代表视图层,负责将数据模型渲染到页面上,ViewModel通过双向绑定把View和Model进行同步交互,不需要手动操作DOM的一种设计思想。2、怎么定义vue-router的动态路由?怎么获取传过来的动态参数?答:在router目录下的index.js文件中,对path属性加上/:id。 使用router对象的params.id3、vue-router有哪https://blog.csdn.net/qq_22182989/article/details/106795502JS必看面试题_勤动手多动脑少说多做厚积薄发-CSDN博客2019JS必看面试题1. javascript的typeof返回哪些数据类型.答案:string,boolean,number,undefined,function,object2. 例举3种强制类型转换和2种隐式类型转换?答案:强制(parseInt,parseFloat,number)隐式(== ===)3. split() join() 的区别答案:前者是将字符串切割成数组的形式,后者是将数组转换成字符串4. 数组方法pop() push() unshift() shttps://blog.csdn.net/qq_22182989/article/details/106792847

20212022最新Web前端经典面试试题及答案-史上最全前端面试题(含答案)_南北极之间-CSDN博客_web前端面试题1、JavaScript数组的常用方法有哪些?答:push、pop、shift、unshift、join、sort、concat、reverse、splice、slice、indexOf等详细资料:JavaScript数组的常用方法总结:遍历,复制,反转,排序,添加,删除(前端常见面试题必考必问_勤动手多动脑少说多做-CSDN博客_js数组倒序输出1. Array.push()向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。一个:var fruits = ["Banana", "https://blog.csdn.net/qq_22182989/article/details/121340087

前端面试题之HTML+CSS(持续更新)_勤动手多动脑少说多做厚积薄发-CSDN博客1.行内元素和块级元素?img算什么?行内元素怎么转化为块级元素?行内元素:和有他元素都在一行上,高度、行高及外边距和内边距都不可改变,文字图片的宽度不可改变,只能容纳文本或者其他行内元素;其中img是行元素块级元素:总是在新行上开始,高度、行高及外边距和内边距都可控制,可以容纳内敛元素和其他元素;行元素转换为块级元素方式:display:block;2.将多个元素设置为同一行?清除浮动有几种方式?将多个元素设置为同一行:float,inline-block清除浮动的方式:方法一:添.https://blog.csdn.net/qq_22182989/article/details/106867530

前端面试题汇总资料:

需要资料的同学可以给我留言

1、JavaScript数组的常用方法有哪些?

答:push、pop、shift、unshift、join、sort、concat、reverse、splice、slice、indexOf等

详细资料:

JavaScript数组的常用方法总结:遍历,复制,反转,排序,添加,删除(前端常见面试题必考必问_勤动手多动脑少说多做厚积薄发-CSDN博客1. Array.push()向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。一个:var fruits = ["Banana", "Orange", "Apple", "Mango"];var x=fruits.push("Kiwi");console.log(fruits)//(5) ["Banana", "Orange", "Apple", "Mango", "Kiwi"] console.log(x)//5 x是 新的数组长度多个:var fruits =https://blog.csdn.net/qq_22182989/article/details/120995392https://blog.csdn.net/qq_22182989/article/details/120995392

2、 js中null和undefined区别?


1. 相同点
  if 判断语句中,两者都会被转换为false

2. 不同点
Number转换的值不同,Number(null)输出为0, Number(undefined)输出为NaN

null表示一个值被定义了,但是这个值是空值
作为函数的参数,表示函数的参数不是对象

作为对象原型链的终点 (Object.getPrototypeOf(Object.prototype))

定义一个值为null是合理的,但定义为undefined不合理(var name = null)      
undefined表示缺少值,即此处应该有值,但是还没有定义
变量被声明了还没有赋值,就为undefined

调用函数时应该提供的参数还没有提供,该参数就等于undefined

对象没有赋值的属性,该属性的值就等于undefined

函数没有返回值,默认返回undefined


描述new操作符具体干了什么?


题目解析

先看代码

var Func=function(){
};
var func=new Func ();
new共经过了4几个阶段

1、创建一个空对象

varobj=new Object();
2、设置原型链

obj.__proto__= Func.prototype;
3、让Func中的this指向obj,并执行Func的函数体。

var result =Func.call(obj);
4、判断Func的返回值类型:

如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

if (typeof(result) == "object"){
  func=result;
}
else{
    func=obj;;
}


js中apply和call的作用和区别是什么?


1、call,apply都属于Function.prototype的一个方法,它是JavaScript引擎内在实现的,因为属于Function.prototype,所以每个Function对象实例(就是每个方法)都有call,apply属性。既然作为方法的属性,那它们的使用就当然是针对方法的了,这两个方法是容易混淆的,因为它们的作用一样,只是使用方式不同。

2、语法:foo.call(this, arg1,arg2,arg3) == foo.apply(this, arguments) == this.foo(arg1, arg2, arg3);

3、相同点:两个方法产生的作用是完全一样的。

4、不同点:方法传递的参数不同。



列举js中的性能优化的方案


包括:
页面级优化和代码级优化。然后细分
页面级优化:比如:
1.减少 HTTP请求数
2.将外部脚本置底(将脚本内容在页面信息内容加载后再加载)
3.异步执行 inline脚本(其实原理和上面是一样,保证脚本在页面内容后面加载。)
4. Lazy Load Javascript(只有在需要加载的时候加载,在一般情况下并不加载信息内容。)
5. 将 CSS放在 HEAD中
6. 异步请求 Callback(就是将一些行为样式提取出来,慢慢的加载信息的内容)
7. 减少不必要的 HTTP跳转
8. 避免重复的资源请求

代码级优化:比如:

js写法
CSS选择符
HTML
Image压缩
参考:

前端性能优化方案都有哪些? - coober - 博客园前端是庞大的,包括 HTML、 CSS、 Javascript、Image 、Flash等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。那么,前端优化的目的是什么 ? 1. 从https://www.cnblogs.com/coober/p/8078847.htmlhttps://www.cnblogs.com/coober/p/8078847.htmlhttps://www.cnblogs.com/coober/p/8078847.html

5.cookies、sessionStorage和localStorage解释及区别


参考:

cookies、sessionStorage和localStorage之间的解释及区别_南北极之间的博客-CSDN博客一、 概念的理解webstorage本地存储1)webstorage是本地存储,存储在客户端,包括localStorage和sessionStorage2)localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信3)sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除。存放数据大小为一般为5MB,而且它仅在客户端(https://blog.csdn.net/qq_22182989/article/details/123493750

7.js延迟加载的方式有哪些?


一般有以下几种方式:

defer 属性
async 属性
动态创建DOM方式
使用jQuery的getScript方法
使用setTimeout延迟方法
让JS最后加载
参考:

es6解构赋值是什么意思?let{a}=B出现的

关于let{a}=B出现的解构赋值_前人挖坑,后人种(pen)-CSDN博客_let {}https://blog.csdn.net/shb2058/article/details/86155830?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbasehttps://blog.csdn.net/shb2058/article/details/86155830?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbasehttps://blog.csdn.net/shb2058/article/details/86155830?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.opensearchhbase

url输入到页面显示全过程?

 浏览器渲染的整个流程

浏览器的整个流程如上图所示。

1、  首先当用户输入一个URL的时候,浏览器就会发送一个请求,请求URL对应的资源。

2、  然后浏览器的HTML解析器会将这个文件解析,并且构建成一棵DOM树。

3、  在构建DOM树的时候,遇到JS和CSS元素,HTML解析器就换将控制权转让给JS解析器或者是CSS解析器。

4、  JS解析器或者是CSS解析器解析完这个元素时候,HTML又继续解析下个元素,直到整棵DOM树构建完成。

5、  DOM树构建完之后,浏览器把DOM树中的一些不可视元素去掉,然后与CSSOM合成一棵render树。

6、  接着浏览器根据这棵render树,计算出各个节点(元素)在屏幕的位置。这个过程叫做layout,输出的是一棵layout树。

7、  最后浏览器根据这棵layout树,将页面渲染到屏幕上去。
 

资料:

前端必会!四步带你吃透浏览器渲染基本原理_CSDN资讯-CSDN博客作者 |SHERlocked93责编 | 胡巍巍大多数设备的刷新频率是60Hz,也就说是浏览器对每一帧画面的渲染工作要在16ms内完成,超出这个时间,页面的渲染就会出现卡顿现象,影响用户体验。前端的用户体验给了前端直观的印象,因此对B/S架构的开发人员来说,熟悉浏览器的内部执行原理显得尤为重要。浏览器主要组成与浏览器线程1.1 浏览器组件浏览器大体上由...https://blog.csdn.net/csdnnews/article/details/95267307https://blog.csdn.net/csdnnews/article/details/95267307https://blog.csdn.net/csdnnews/article/details/95267307

前端面试-浏览器渲染机制_C7-CSDN博客在开发WEB应用整一个流程中,用户体验通常都会被提及,而网站的性能,又是与用户体验直接挂钩,因此可以在项目需求规格文档中经常看到项目对性能的需求。WEB性能优化常见的入手点包括以下几点:1、  HTTP请求2、  服务器响应速度3、  浏览器响应速度其中浏览器响应速度包括渲染速度和代码执行的速度。而本篇文章的目标主要集中在第三点,先整体了解一下浏览器的渲染机制,再通过这个机https://blog.csdn.net/cde7070/article/details/50619853https://blog.csdn.net/cde7070/article/details/50619853https://blog.csdn.net/cde7070/article/details/50619853

前端面试题之ES6新特性

我们来看看ES6都做了哪些扩展。

变量的解构赋值

字符串,数值的扩展:

数组,对象,函数的扩展:

资料:

JavaScript ES6 (w3schools.com)https://www.w3schools.com/js/js_es6.asp

ES6新特性 - 简书ES6语法 Tags: javascript [TOC] const 与 let 变量 使用var带来的麻烦: 运行getClothing(false)后输出的是undefi...https://www.jianshu.com/p/87008f4f8513https://www.jianshu.com/p/87008f4f8513

ES6中有哪些新特性?_bradmatt的博客-CSDN博客_es6的新特性有哪些ES6中的新特性(一)ECMAScript6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。我们来看看ES6都做了哪些扩展。Let和const关键字变量的解构赋值字符串,数值的扩展数组,对象的扩展函数的扩展for...of先从let和const...https://blog.csdn.net/bradmatt/article/details/80920153https://blog.csdn.net/bradmatt/article/details/80920153

css垂直居中的问题:所有的方法都在这里了

资料:

CSS垂直居中,你会多少种写法?_携手天下-CSDN博客  CSS控制居中是前端开发中非常常用的布局技能,本文列出几种CSS控制元素居中的几种方法。  谈及HTML元素居中展示,涉及到水平居中和垂直居中,以及水平垂直居中。由于HTML文档流是水平方向的,所以水平方向上的布局控制比垂直方向要简单很多,居中也是如此。不过(水平)垂直居中还是有很多种写法,至少一只手是数不过来了,本文列出几种,并进行简单比较。一、水平居中  使用CSS控制水平居中很简单...https://blog.csdn.net/u010419337/article/details/89529541https://blog.csdn.net/u010419337/article/details/89529541

CSS垂直居中,你会多少种写法?_携手天下-CSDN博客https://blog.csdn.net/u010419337/article/details/89529541

说说vue的双向数据绑定实现原理(其实今年已经很少问到这个了,但是有些公司的面试官还是会问到)

通过Object.definerProperty来劫持各个数据的属性的setter和getter,在数据变化时,发布消息给依赖收集器,通知观察者去执行回调函数,达到视图更新的效果。(但是使用Object.definerProperty实现监听时是有一些痛点的,比如,①无法监测数组下标变化,导致数组删除或者插入元素时,数组的变化无法实时响应;②只能对对象的属性进行监测,当对象深度比较深时,只能遍历每个属性来实现监听。vue3.0采用的Proxy,就完全避开了Object.definerProperty方法的这些痛点)

vue如何在组件之间进行传值

propsemitvuex、路由传参、通过本地存储传参、vue-bus(事件巴士)、 $refs$children$parent

vuex和vue的双向数据绑定有什么冲突

在严格模式下vuex中的state对象中的属性是不能随意更改的,但是在表单处理时使用v-model时用户可以随意更改数据,如果vuex中的state中的属性直接绑在v-model中时会抛出一个错误。解决办法

vue-router中的history模式和hash模式的区别

说一下vue的生命周期(回答的时候,顺便说一下哪个钩子函数实现了什么功能)

beforeCreate:初始化事件,进行数据观测。

created:data数据进行绑定。

beforeMount:虚拟DOM替换真实DOM。

mounted:将DOM元素挂载到页面。

beforeUpdate: data数据更新之前。

updated: data数据更新完成之后。

beforeDestroy:在实例销毁之前调用,所有实例仍可以调用。

destroyed:在实例销毁之后调用,所有实例被销毁。

路由懒加载的作用

懒加载即在需要的时候才进行加载,随用随载。在单页面应用中,如果没有应用懒加载,webpack打包后的文件会非常大,导致第一次进入首页时,加载时间过长,不利于用户体验。而运用懒加载可以将页面进行划分,需要的时候才加载页面,可以有效的分担首页所承受的加载压力,有效减少了加载用时。

自定义指令

混入(Mixin)

父子组件生命周期的执行顺序

如何阻止组件间的样式相互干扰

给style标签添加scoped属性

对虚拟dom的了解,说说实现原理

有用过axios吗?axios是如何设置拦截器的?

vue中如何监听数组的变化?

Object.definerProperty是无法对数组和对象进行劫持监听的,所以vue3.0以下(不包括3.0)版本,对数组的监听是直接重写了push、shift……等Array常用的数组操作。

谈谈keep-alive

vue-router路由钩子

vue和react的区别

说说微信小程序的生命周期

微信小程序的实现原理

微信小程序的自定义组件

你在微信小程序中遇到什么坑,最后是怎么解决的?

微信小程序是否有可以跳转其他程序的方法

节点的插入操作有哪些

如何解决跨域(如果提到jsonp,一般面试官都会顺嘴问句:jsonp的实现原理)

typeOf和instanceOf的区别

如何区别Object和Array

深拷贝和浅拷贝的实现原理,以及为什么要深拷贝

谈谈你对闭包的理解(谈到闭包的话,一般会顺势问到防抖和节流。有些面试官会直接问,有些面试官会说一个试用场景,让你回答用什么方法解决。)

谈谈你对原型链的理解

冒泡和捕获

用js实现一个promise

用js实现new方法

websoket的实现原理

直播的推流原理

post请求和get请求的区别

简述ajax的过程

js数据类型有哪些?

谈谈js原型

给一个盒子水平垂直居中的方法

css中的布局有哪些

箭头函数的作用是什么

promise的作用和实现原理

async和promise的区别

let,const和var的区别

var存在变量声明提升,var在{}中是没有作用域的,var定义的变量名可以重复命名。而这些let和const都做了限制

map和forEach的区别

map和forEach都能达到遍历数组的目的。不过map会返回一个新的和原来数组长度一样的数组,而forEech没有返回值

做过哪些性能优化

移动端自适应

http和https的区别(个别面试官会问到其中的原理)

代码规范化是否有必要

发布订阅者模式和观察者模式的区别

请写出从“在浏览器输入域名”到“页面静态资源完全加载”的整个流程

堆和栈的区别

webpack的优点(有些会问某个具体的配置,比如我就被问了sass解析成css要怎么配置的)

你们公司项目开始到项目结束的流程

对项目架构方面有了解吗?

HTML5的新特性都有哪些?

一、数据类型

1. JavaScript有哪些数据类型,它们的区别?

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出。
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

2. 数据类型检测的方式有哪些

(1)typeof

  1. console.log(typeof 2); // number
  2. console.log(typeof true); // boolean
  3. console.log(typeof 'str'); // string
  4. console.log(typeof []); // object
  5. console.log(typeof function(){}); // function
  6. console.log(typeof {}); // object
  7. console.log(typeof undefined); // undefined
  8. console.log(typeof null); // object
  9. 复制代码

其中数组、对象、null都会被判断为object,其他判断都正确。

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

  1. console.log(2 instanceof Number); // false
  2. console.log(true instanceof Boolean); // false
  3. console.log('str' instanceof String); // false
  4. console.log([] instanceof Array); // true
  5. console.log(function(){} instanceof Function); // true
  6. console.log({} instanceof Object); // true
  7. 复制代码

可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

(3) constructor

  1. console.log((2).constructor === Number); // true
  2. console.log((true).constructor === Boolean); // true
  3. console.log(('str').constructor === String); // true
  4. console.log(([]).constructor === Array); // true
  5. console.log((function() {}).constructor === Function); // true
  6. console.log(({}).constructor === Object); // true
  7. 复制代码

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

  1. function Fn(){};
  2. Fn.prototype = new Array();
  3. var f = new Fn();
  4. console.log(f.constructor===Fn); // false
  5. console.log(f.constructor===Array); // true
  6. 复制代码

(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

  1. var a = Object.prototype.toString;
  2. console.log(a.call(2));
  3. console.log(a.call(true));
  4. console.log(a.call('str'));
  5. console.log(a.call([]));
  6. console.log(a.call(function(){}));
  7. console.log(a.call({}));
  8. console.log(a.call(undefined));
  9. console.log(a.call(null));
  10. 复制代码

同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

3. 判断数组的方式有哪些

  • 通过Object.prototype.toString.call()做判断
  1. Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  2. 复制代码
  • 通过原型链做判断
  1. obj.__proto__ === Array.prototype;
  2. 复制代码
  • 通过ES6的Array.isArray()做判断
  1. Array.isArrray(obj);
  2. 复制代码
  • 通过instanceof做判断
  1. obj instanceof Array
  2. 复制代码
  • 通过Array.prototype.isPrototypeOf
  1. Array.prototype.isPrototypeOf(obj)
  2. 复制代码

4. null和undefined区别

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

5. typeof null 的结果是什么,为什么?

typeof null 的结果是Object。

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

  1. 000: object - 当前存储的数据指向一个对象。
  2. 1: int - 当前存储的数据是一个 31 位的有符号整数。
  3. 010: double - 当前存储的数据指向一个双精度的浮点数。
  4. 100: string - 当前存储的数据指向一个字符串。
  5. 110: boolean - 当前存储的数据是布尔值。
  6. 复制代码

如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。

有两种特殊数据类型:

  • undefined的值是 (-2)30(一个超出整数范围的数字);
  • null 的值是机器码 NULL 指针(null 指针的值全是 0)

那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。

6. intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

  1. function myInstanceof(left, right) {
  2. // 获取对象的原型
  3. let proto = Object.getPrototypeOf(left)
  4. // 获取构造函数的 prototype 对象
  5. let prototype = right.prototype;
  6. // 判断构造函数的 prototype 对象是否在对象的原型链上
  7. while (true) {
  8. if (!proto) return false;
  9. if (proto === prototype) return true;
  10. // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
  11. proto = Object.getPrototypeOf(proto);
  12. }
  13. }
  14. 复制代码

7. 为什么0.1+0.2 ! == 0.3,如何让其相等  

在开发过程中遇到类似这样的问题:

  1. let n1 = 0.1, n2 = 0.2
  2. console.log(n1 + n2) // 0.30000000000000004
  3. 复制代码

这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:

  1. (n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
  2. 复制代码

toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?

计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?

一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。

根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004

下面看一下双精度数是如何保存的:

  • 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0表示正数,占用1位
  • 第二部分(绿色):用来存储指数(exponent),占用11位
  • 第三部分(红色):用来存储小数(fraction),占用52位

对于0.1,它的二进制为:

  1. 0.00011001100110011001100110011001100110011001100110011001 10011...
  2. 复制代码

转为科学计数法(科学计数法的结果就是浮点数):

  1. 1.1001100110011001100110011001100110011001100110011001*2^-4
  2. 复制代码

可以看出0.1的符号位为0,指数位为-4,小数位为:

  1. 1001100110011001100110011001100110011001100110011001
  2. 复制代码

那么问题又来了,指数位是负数,该如何保存呢?

IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023

  • 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013
  • 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
  • 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。

对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.

所以,0.1表示为:

  1. 0 1111111011 1001100110011001100110011001100110011001100110011001
  2. 复制代码

说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?

对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3

  1. function numberepsilon(arg1,arg2){
  2. return Math.abs(arg1 - arg2) < Number.EPSILON;
  3. }
  4. console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
  5. 复制代码

8. 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。

9. typeof NaN 的结果是什么?

NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

  1. typeof NaN; // "number"
  2. 复制代码

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

10. isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

11. == 操作符的强制类型转换规则?

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 xy 是否相同,就会进行如下判断流程:

  1. 首先会判断两者类型是否**相同,**相同的话就比较两者的大小;
  2. 类型不相同的话,就会进行类型转换;
  3. 会先判断是否在对比 nullundefined,是的话就会返回 true
  4. 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number
  1. 1 == '1'
  2. 1 == 1
  3. 复制代码
  1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
  1. '1' == true
  2. '1' == 1
  3. 1 == 1
  4. 复制代码
  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断
  1. '1' == { name: 'js' } ↓'1' == '[object Object]'
  2. 复制代码

其流程图如下:

12. 其他值到字符串的转换规则?

  • Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

13. 其他值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

14. 其他值到布尔类型的值的转换规则?

以下这些是假值: • undefined • null • false • +0、-0 和 NaN • ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

15. || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

16. Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

17. 什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

  1. const a = "abc";
  2. a.length; // 3
  3. a.toUpperCase(); // "ABC"
  4. 复制代码

在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。

JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

  1. var a = 'abc'
  2. Object(a) // String {"abc"}
  3. 复制代码

也可以使用valueOf方法将包装类型倒转成基本类型:

  1. var a = 'abc'
  2. var b = Object(a)
  3. var c = b.valueOf() // 'abc'
  4. 复制代码

看看如下代码会打印出什么:

  1. var a = new Boolean( false );
  2. if (!a) {
  3. console.log( "Oops" ); // never runs
  4. }
  5. 复制代码

答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

18. JavaScript 中如何进行隐式类型转换?

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

  1. /**
  2. * @obj 需要转换的对象
  3. * @type 期望的结果类型
  4. */
  5. ToPrimitive(obj,type)
  6. 复制代码

type的值为number或者string

(1)当typenumber时规则如下:

  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;
  • 调用objtoString方法,后续同上;
  • 抛出TypeError 异常。

(2)当typestring时规则如下:

  • 调用objtoString方法,如果为原始值,则返回,否则下一步;
  • 调用objvalueOf方法,后续同上;
  • 抛出TypeError 异常。

可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string
  • 其他情况下,type默认为number

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

  1. var objToNumber = value => Number(value.valueOf().toString())
  2. objToNumber([]) === 0
  3. objToNumber({}) === NaN
  4. 复制代码

而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

  1. +操作符

+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。

  1. 1 + '23' // '123'
  2. 1 + false // 1
  3. 1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
  4. '1' + false // '1false'
  5. false + true // 1
  6. 复制代码
  1. -*\操作符

NaN也是一个数字

  1. 1 * '23' // 23
  2. 1 * false // 0
  3. 1 / 'aa' // NaN
  4. 复制代码
  1. 对于==操作符

操作符两边的值都尽量转成number

  1. 3 == true // false, 3 转为number3true转为number1
  2. '0' == false //true, '0'转为number0false转为number0
  3. '0' == 0 // '0'转为number0
  4. 复制代码
  1. 对于<>比较符

如果两边都是字符串,则比较字母表顺序:

  1. 'ca' < 'bd' // false
  2. 'a' < 'b' // true
  3. 复制代码

其他情况下,转换为数字再比较:

  1. '12' < 13 // true
  2. false > -1 // true
  3. 复制代码

以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:

  1. var a = {}
  2. a > 2 // false
  3. 复制代码

其对比过程如下:

  1. a.valueOf() // {}, 上面提到过,ToPrimitive默认typenumber,所以先valueOf,结果还是个对象,下一步
  2. a.toString() // "[object Object]",现在是一个字符串了
  3. Number(a.toString()) // NaN,根据上面 <> 操作符的规则,要转换成数字
  4. NaN > 2 //false,得出比较结果
  5. 复制代码

又比如:

  1. var a = {name:'Jack'}
  2. var b = {age: 18}
  3. a + b // "[object Object][object Object]"
  4. 复制代码

运算过程如下:

  1. a.valueOf() // {},上面提到过,ToPrimitive默认typenumber,所以先valueOf,结果还是个对象,下一步
  2. a.toString() // "[object Object]"
  3. b.valueOf() // 同理
  4. b.toString() // "[object Object]"
  5. a + b // "[object Object][object Object]"
  6. 复制代码

19. + 操作符什么时候用于字符串的拼接?

根据 ES5 规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。如果不能转换为字符串,则会将其转换为数字类型来进行计算。

简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。

20. 为什么会有BigInt的提案?

JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。

21. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别

扩展运算符:

  1. let outObj = {
  2. inObj: {a: 1, b: 2}
  3. }
  4. let newObj = {...outObj}
  5. newObj.inObj.a = 2
  6. console.log(outObj) // {inObj: {a: 2, b: 2}}
  7. 复制代码

Object.assign():

  1. let outObj = {
  2. inObj: {a: 1, b: 2}
  3. }
  4. let newObj = Object.assign({}, outObj)
  5. newObj.inObj.a = 2
  6. console.log(outObj) // {inObj: {a: 2, b: 2}}
  7. 复制代码

可以看到,两者都是浅拷贝。

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。

二、ES6

1. let、const、var的区别

(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

区别varletconst
是否有块级作用域×✔️✔️
是否存在变量提升✔️××
是否添加全局属性✔️××
能否重复声明变量✔️××
是否存在暂时性死区×✔️✔️
是否必须设置初始值××✔️
能否改变指针指向✔️✔️×

2. const对象的属性可以修改吗

const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

3. 如果new一个箭头函数的会怎么样

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的。

4. 箭头函数与普通函数的区别

(1)箭头函数比普通函数更加简洁

  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号
  • 如果有多个参数,用逗号分割
  • 如果函数体的返回值只有一句,可以省略大括号
  • 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
  1. let fn = () => void doesNotReturn();
  2. 复制代码

(2)箭头函数没有自己的this

箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。

(3)箭头函数继承来的this指向永远不会改变

  1. var id = 'GLOBAL';
  2. var obj = {
  3. id: 'OBJ',
  4. a: function(){
  5. console.log(this.id);
  6. },
  7. b: () => {
  8. console.log(this.id);
  9. }
  10. };
  11. obj.a(); // 'OBJ'
  12. obj.b(); // 'GLOBAL'
  13. new obj.a() // undefined
  14. new obj.b() // Uncaught TypeError: obj.b is not a constructor
  15. 复制代码

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向

  1. var id = 'Global';
  2. let fun1 = () => {
  3. console.log(this.id)
  4. };
  5. fun1(); // 'Global'
  6. fun1.call({id: 'Obj'}); // 'Global'
  7. fun1.apply({id: 'Obj'}); // 'Global'
  8. fun1.bind({id: 'Obj'})(); // 'Global'
  9. 复制代码

(5)箭头函数不能作为构造函数使用

构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(6)箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

(7)箭头函数没有prototype

(8)箭头函数不能用作Generator函数,不能使用yeild关键字

5. 箭头函数的this指向哪⾥?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

可以⽤Babel理解⼀下箭头函数:

  1. // ES6
  2. const obj = {
  3. getArrow() {
  4. return () => {
  5. console.log(this === obj);
  6. };
  7. }
  8. }
  9. 复制代码

转化后:

  1. // ES5,由 Babel 转译
  2. var obj = {
  3. getArrow: function getArrow() {
  4. var _this = this;
  5. return function () {
  6. console.log(_this === obj);
  7. };
  8. }
  9. };
  10. 复制代码

6. 扩展运算符的作用及使用场景

(1)对象扩展运算符

对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

  1. let bar = { a: 1, b: 2 };
  2. let baz = { ...bar }; // { a: 1, b: 2 }
  3. 复制代码

上述方法实际上等价于:

  1. let bar = { a: 1, b: 2 };
  2. let baz = Object.assign({}, bar); // { a: 1, b: 2 }
  3. 复制代码

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。

同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

  1. let bar = {a: 1, b: 2};
  2. let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
  3. 复制代码

利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。

需要注意:扩展运算符对对象实例的拷贝属于浅拷贝

(2)数组扩展运算符

数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

  1. console.log(...[1, 2, 3])
  2. // 1 2 3
  3. console.log(...[1, [2, 3, 4], 5])
  4. // 1 [2, 3, 4] 5
  5. 复制代码

下面是数组的扩展运算符的应用:

  • 将数组转换为参数序列
  1. function add(x, y) {
  2. return x + y;
  3. }
  4. const numbers = [1, 2];
  5. add(...numbers) // 3
  6. 复制代码
  • 复制数组
  1. const arr1 = [1, 2];
  2. const arr2 = [...arr1];
  3. 复制代码

要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

  • 合并数组

如果想在数组内合并数组,可以这样:

  1. const arr1 = ['two', 'three'];const arr2 = ['one', ...arr1, 'four', 'five'];// ["one", "two", "three", "four", "five"]
  2. 复制代码
  • 扩展运算符与解构赋值结合起来,用于生成数组
  1. const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest // [2, 3, 4, 5]
  2. 复制代码

需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

  1. const [...rest, last] = [1, 2, 3, 4, 5]; // 报错const [first, ...rest, last] = [1, 2, 3, 4, 5]; // 报错
  2. 复制代码
  • 将字符串转为真正的数组
  1. [...'hello'] // [ "h", "e", "l", "l", "o" ]
  2. 复制代码
  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组

比较常见的应用是可以将某些数据结构转为数组:

  1. // arguments对象
  2. function foo() {
  3. const args = [...arguments];
  4. }
  5. 复制代码

用于替换es5中的Array.prototype.slice.call(arguments)写法。

  • 使用Math函数获取数组中特定的值
  1. const numbers = [9, 4, 7, 1];
  2. Math.min(...numbers); // 1
  3. Math.max(...numbers); // 9
  4. 复制代码

7. Proxy 可以实现什么功能?

在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。

Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

  1. let p = new Proxy(target, handler)
  2. 复制代码

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

下面来通过 Proxy 来实现一个数据响应式:

  1. let onWatch = (obj, setBind, getLogger) => {
  2. let handler = {
  3. get(target, property, receiver) {
  4. getLogger(target, property)
  5. return Reflect.get(target, property, receiver)
  6. },
  7. set(target, property, value, receiver) {
  8. setBind(value, property)
  9. return Reflect.set(target, property, value)
  10. }
  11. }
  12. return new Proxy(obj, handler)
  13. }
  14. let obj = { a: 1 }
  15. let p = onWatch(
  16. obj,
  17. (v, property) => {
  18. console.log(`监听到属性${property}改变为${v}`)
  19. },
  20. (target, property) => {
  21. console.log(`'${property}' = ${target[property]}`)
  22. }
  23. )
  24. p.a = 2 // 监听到属性a改变
  25. p.a // 'a' = 2
  26. 复制代码

在上述代码中,通过自定义 setget 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。

当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。

8. 对对象与数组的解构的理解

解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。 1)数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据的:

  1. const [a, b, c] = [1, 2, 3]
  2. 复制代码

最终,a、b、c分别被赋予了数组第0、1、2个索引位的值: 数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:

  1. const [a,,c] = [1,2,3]
  2. 复制代码

通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:

2)对象的解构 对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

  1. const stu = {
  2. name: 'Bob',
  3. age: 24
  4. }
  5. 复制代码

假如想要解构它的两个自有属性,可以这样:

  1. const { name, age } = stu
  2. 复制代码

这样就得到了 name 和 age 两个和 stu 平级的变量:

注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:

  1. const { age, name } = stu
  2. 复制代码

9. 如何提取高度嵌套的对象里的指定属性?

有时会遇到一些嵌套程度非常深的对象:

  1. const school = {
  2. classes: {
  3. stu: {
  4. name: 'Bob',
  5. age: 24,
  6. }
  7. }
  8. }
  9. 复制代码

像此处的 name 这个变量,嵌套了四层,此时如果仍然尝试老方法来提取它:

  1. const { name } = school
  2. 复制代码

显然是不奏效的,因为 school 这个对象本身是没有 name 这个属性的,name 位于 school 对象的“儿子的儿子”对象里面。要想把 name 提取出来,一种比较笨的方法是逐层解构:

  1. const { classes } = school
  2. const { stu } = classes
  3. const { name } = stu
  4. name // 'Bob'
  5. 复制代码

但是还有一种更标准的做法,可以用一行代码来解决这个问题:

  1. const { classes: { stu: { name } }} = school
  2. console.log(name) // 'Bob'
  3. 复制代码

可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。

10. 对 rest 参数的理解

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组

  1. function mutiple(...args) {
  2. let result = 1;
  3. for (var val of args) {
  4. result *= val;
  5. }
  6. return result;
  7. }
  8. mutiple(1, 2, 3, 4) // 24
  9. 复制代码

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

  1. function mutiple(...args) {
  2. console.log(args)
  3. }
  4. mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
  5. 复制代码

这就是 … rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

11. ES6中模板语法与字符串处理

ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:

  1. var name = 'css'
  2. var career = 'coder'
  3. var hobby = ['coding', 'writing']
  4. var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]
  5. 复制代码

仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:

  1. var name = 'css'
  2. var career = 'coder'
  3. var hobby = ['coding', 'writing']
  4. var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
  5. 复制代码

字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:

  • 在模板字符串中,空格、缩进、换行都会被保留
  • 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算

基于第一点,可以在模板字符串里无障碍地直接写 html 代码:

  1. let list = `
  2. <ul>
  3. <li>列表项1</li>
  4. <li>列表项2</li>
  5. </ul>
  6. `;
  7. console.log(message); // 正确输出,不存在报错
  8. 复制代码

基于第二点,可以把一些简单的计算和调用丢进 ${} 来做:

  1. function add(a, b) {
  2. const finalString = `${a} + ${b} = ${a+b}`
  3. console.log(finalString)
  4. }
  5. add(1, 2) // 输出 '1 + 2 = 3'
  6. 复制代码

除了模板语法外, ES6中还新增了一系列的字符串方法用于提升开发效率:

(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。

  • includes:判断字符串与子串的包含关系:
  1. const son = 'haha'
  2. const father = 'xixi haha hehe'
  3. father.includes(son) // true
  4. 复制代码
  • startsWith:判断字符串是否以某个/某串字符开头:
  1. const father = 'xixi haha hehe'
  2. father.startsWith('haha') // false
  3. father.startsWith('xixi') // true
  4. 复制代码
  • endsWith:判断字符串是否以某个/某串字符结尾:
  1. const father = 'xixi haha hehe'
  2. father.endsWith('hehe') // true
  3. 复制代码

(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):

  1. const sourceCode = 'repeat for 3 times;'
  2. const repeated = sourceCode.repeat(3)
  3. console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
  4. 复制代码

三、JavaScript基础

1. new操作符的实现原理

new操作符的执行过程:

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

  1. function objectFactory() {
  2. let newObject = null;
  3. let constructor = Array.prototype.shift.call(arguments);
  4. let result = null;
  5. // 判断参数是否是一个函数
  6. if (typeof constructor !== "function") {
  7. console.error("type error");
  8. return;
  9. }
  10. // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  11. newObject = Object.create(constructor.prototype);
  12. // 将 this 指向新建对象,并执行函数
  13. result = constructor.apply(newObject, arguments);
  14. // 判断返回对象
  15. let flag = result && (typeof result === "object" || typeof result === "function");
  16. // 判断返回结果
  17. return flag ? result : newObject;
  18. }
  19. // 使用方法
  20. objectFactory(构造函数, 初始化参数);
  21. 复制代码

2. map和Object的区别

MapObject
意外的键Map默认情况不包含任何键,只包含显式插入的键。Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型Map的键可以是任意值,包括函数、对象或任意基本类型。Object 的键必须是 String 或是Symbol。
键的顺序Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。Object 的键是无序的
SizeMap 的键值对个数可以轻易地通过size 属性获取Object 的键值对个数只能手动计算
迭代Map 是 iterable 的,所以可以直接被迭代。迭代Object需要以某种方式获取它的键然后才能迭代。
性能在频繁增删键值对的场景下表现更好。在频繁添加和删除键值对的场景下未作出优化。

3. map和weakMap的区别

(1)Map map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。

实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:

  1. const map = [
  2. ["name","张三"],
  3. ["age",18],
  4. ]
  5. 复制代码

Map数据结构有以下操作方法:

  • sizemap.size 返回Map结构的成员总数。
  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
  • clear():map.clear()清除所有成员,没有返回值。

Map结构原生提供是三个遍历器生成函数和一个遍历方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • forEach():遍历Map的所有成员。
  1. const map = new Map([
  2. ["foo",1],
  3. ["bar",2],
  4. ])
  5. for(let key of map.keys()){
  6. console.log(key); // foo bar
  7. }
  8. for(let value of map.values()){
  9. console.log(value); // 1 2
  10. }
  11. for(let items of map.entries()){
  12. console.log(items); // ["foo",1] ["bar",2]
  13. }
  14. map.forEach( (value,key,map) => {
  15. console.log(key,value); // foo 1 bar 2
  16. })
  17. 复制代码

(2)WeakMap WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

该对象也有以下几种方法:

  • set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
  • get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
  • has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
  • delete(key):该方法删除某个键,返回true,如果删除失败,返回false。

其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

总结:

  • Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
  • WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。

4. JavaScript有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。

标准内置对象的分类:

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等

(10)控制抽象对象 例如 Promise、Generator 等

(11)反射。例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他。例如 arguments

总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

5. 常用的正则表达式有哪些?

  1. //1)匹配 16 进制颜色值
  2. var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
  3. //2)匹配日期,如 yyyy-mm-dd 格式
  4. var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
  5. //3)匹配 qq 号
  6. var regex = /^[1-9][0-9]{4,10}$/g;
  7. //4)手机号码正则
  8. var regex = /^1[34578]\d{9}$/g;
  9. //5)用户名正则
  10. var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
  11. 复制代码

6. 对JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

  • JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

7. JavaScript脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

8. JavaScript 类数组对象的定义?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

常见的类数组转换为数组的方法有这样几种:

(1)通过 call 调用数组的 slice 方法来实现转换

  1. Array.prototype.slice.call(arrayLike);
  2. 复制代码

(2)通过 call 调用数组的 splice 方法来实现转换

  1. Array.prototype.splice.call(arrayLike, 0);
  2. 复制代码

(3)通过 apply 调用数组的 concat 方法来实现转换

  1. Array.prototype.concat.apply([], arrayLike);
  2. 复制代码

(4)通过 Array.from 方法来实现转换

  1. Array.from(arrayLike);
  2. 复制代码

9. 数组有哪些原生方法?

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 数组归并方法 reduce() 和 reduceRight() 方法

10. Unicode、UTF-8、UTF-16、UTF-32的区别?

(1)Unicode

在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。

  • 它是基于拉丁字母的一套电脑编码系统。
  • 它定义了一个用于代表常见字符的字典。
  • 它包含了"A-Z"(包含大小写),数据"0-9" 以及一些常见的符号。
  • 它是专门为英语而设计的,有128个编码,对其他语言无能为力

ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说UnicodeASCII 的超集。

Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode的实现方式(也就是编码方式)有很多种,常见的是UTF-8UTF-16UTF-32USC-2

(2)UTF-8

UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。

注意: UTF-8 是一种编码方式,Unicode是一个字符集合。

UTF-8的编码规则:

  • 对于单字节的符号,字节的第一位为0,后面的7位为这个字符的Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。
  • 对于n字节的符号,第一个字节的前n位都是1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的Unicode码 。

来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :

编码范围(编号对应的十进制数)二进制格式
0x00—0x7F (0-127)0xxxxxxx
0x80—0x7FF (128-2047)110xxxxx 10xxxxxx
0x800—0xFFFF  (2048-65535)1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF  (65536以上)11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:

  • 找到该Unicode编码的所在的编号范围,进而找到与之对应的二进制格式
  • Unicode编码转换为二进制数(去掉最高位的0)
  • 将二进制数从右往左一次填入二进制格式的X中,如果有X未填,就设为0

来看一个实际的例子: “” 字的Unicode编码是:0x9A6C,整数编号是39532 (1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx (2)39532对应的二进制数为1001 1010 0110 1100 (3)将二进制数填入X中,结果是:11101001 10101001 10101100

(3)UTF-16

1. 平面的概念

在了解UTF-16之前,先看一下平面的概念: Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(216)个字符,这称为一个平面,目前总共有17 个平面。

最前面的一个平面称为基本平面,它的码点从0 — 216-1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000—U+10FFFF

2. UTF-16 概念:

UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。

3. UTF-16 编码规则:

  • 编号在 U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。
  • 编号在 U+10000—U+10FFFF 之间的字符,需要用四个字节表示。

4. 编码识别

那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?

UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。

辅助平面共有 220 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。

因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。

5. 举例说明

以 " 本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】

推荐阅读
相关标签