2024前端高频面试题
- HTML
- 1. 什么是 DOCTYPE, 有何作用?
- 2. 说说对 html 语义化的理解
- 3. 前端页面有哪三层构成,分别是什么?
- 4. 什么是严格模式与混杂模式?
- 5. 对于Web标准以及W3C的理解
- 6. 行级元素和块级元素分别有哪些及怎么转换?
- 7. H5有哪些新元素和新特性
- 8. 如何使用HTML5中的Canvas元素绘制图形?
- 9. cookie、sessionStorage 和 localStorage 的区别
- 10. script、script async 和 script defer 的区别
- 11. iframe的作用以及优缺点
- 12. Quirks(怪癖)模式是什么?它和Standards(标准)有什么区别?
- 13. meta viewport 是做什么用的,怎么写?
- 14.ES5和ES6的区别?
- JS
- 1. js基本数据类型有哪些及它们的区别
- 2. 数据类型检测的方式有哪些
- 3. 判断数组的方式有哪些
- 4. 请简述JavaScript中的this
- 5. 箭头函数与普通函数有什么区别
- 6. AMD和CommonJS的区别
- 7. ES6模块与CommonJS模块有什么异同?
- 8. let、const、var的区别
- 9.new操作符的实现原理
- 10. 数组有哪些原生方法?
- 11. for in和for of的区别
- 12. 数组的遍历方法有哪些?
- 13. forEach和map的区别
- 14. 原型和原型链
- 15. 对执行上下文,作用域(链),闭包的理解
- 16. 实现call、apply 及 bind 函数
- 17. call、apply 函数的区别
- 18. 异步编程的实现方式?
- 19. setTimeout、Promise、Async/Await 的区别
- 20. Promise.all和Promise.race的区别的使用场景
- 21. 对async/await 的理解
- 22. 浏览器的垃圾回收机制
- 23. 哪些情况会导致内存泄漏
- 24. ES6有哪些新特性
- 25. 匿名函数的典型应用场景是什么?
- 26.你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处
- 27.什么是事件循环?调用堆栈和任务队列之间有什么区别?
- 28.js设计模式有哪些?
- Vue
- 1. vue的生命周期有哪些及每个生命周期做了什么?
- 2. vue响应式原理是什么?vue3的响应式有何不同
- 3. vue3和vue2的区别
- 4. 谈一谈对 MVVM 的理解?
- 5. 在 Vue2.x 中如何检测数组的变化?
- 6. v-model 双向绑定的原理是什么?
- 7.vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?
- 8. vue组件通信方式有哪些及原理
- 9. Vue的路由实现, hash路由和history路由实现原理说一下
- 10. 说一下 v-if 与 v-show 的区别
- 11. keep-alive的常用属性有哪些及实现原理
- 12. nextTick 的作用是什么?他的实现原理是什么?
- 13. 说一下 Vue SSR 的实现原理
- 14. Vue 组件的 data 为什么必须是函数
- 15. 说一下 Vue 的 computed 的实现原理
- 16. 说一下 Vue complier 的实现原理是什么样的?
- 17. Vue 与 Angular 以及 React 的区别是什么?
- 18. 说一下 watch 与 computed 的区别是什么?以及他们的使用场景分别是什么?
- 19.说一下你知道的 vue 修饰符都有哪些?
- 20. 如何实现 vue 项目中的性能优化?
- 21. vue 中的 spa 应用如何优化首屏加载速度?
- 22. Vue 中的 Key 的作用是什么?
- 23. 组件中写 name 选项有哪些好处
- 24. 说一下 ref 的作用是什么?
- 25. 你的接口请求一般放在哪个生命周期中?为什么要这样做?
- CSS
HTML
1. 什么是 DOCTYPE, 有何作用?
Doctype是HTML5的文档声明,通过它可以告诉浏览器,使用哪一个HTML版本标准解析文档。在浏览器发展的过程中,HTML出现过很多版本,不同的版本之间格式书写上略有差异。如果没有事先告诉浏览器,那么浏览器就不知道文档解析标准是什么?此时,大部分浏览器将开启最大兼容模式来解析网页,我们一般称为怪异模式,这不仅会降低解析效率,而且会在解析过程中产生一些难以预知的bug,所以文档声明是必须的
2. 说说对 html 语义化的理解
HTML标签的语义化,简单来说,就是用正确的标签做正确的事情,给某块内容用上一个最恰当最合适的标签,使页面有良好的结构,页面元素有含义,无论是谁都能看懂这块内容是什么。
语义化的优点如下:
- 在没有CSS样式情况下也能够让页面呈现出清晰的结构
- 有利于SEO和搜索引擎建立良好的沟通,有助于爬虫抓取更多的有效信息,爬虫是依赖于标签来确定上下文和各个关键字的权重
- 方便团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化。
3. 前端页面有哪三层构成,分别是什么?
构成:结构层、表示层、行为层
结构层(structural layer)
- 结构层类似于盖房子需要打地基以及房子的悬梁框架,它是由HTML超文本标记语言来创建的,也就是页面中的各种标签,在结构层中保存了用户可以看到的所有内容,比如说:一段文字、一张图片、一段视频等等。
表示层(presentation layer)
- 表示层是由CSS负责创建,它的作用是如何显示有关内容,学名:层叠样式表,也就相当于装修房子,看你要什么风格的,田园的、中式的、地中海的,总之CSS都能办妥。
行为层(behaviorlayer)
- 行为层表示网页内容跟用户之间产生交互性,简单来说就是用户操作了网页,网页给用户一个反馈,这是JavaScript和DOM主宰的领域。
4. 什么是严格模式与混杂模式?
严格模式:是以浏览器支持的最高标准运行
混杂模式:页面以宽松向下兼容的方式显示,模拟老式浏览器的行为
5. 对于Web标准以及W3C的理解
Web标准简单来说可以分为结构、表现、行为。其中结构是由HTML各种标签组成,简单来说就是body里面写入标签是为了页面的结构。表现指的是CSS层叠样式表,通过CSS可以让我们的页面结构标签更具美感。行为指的是页面和用户具有一定的交互,这部分主要由JS组成。
W3C,全称:world wide web consortium是一个制定各种标准的非盈利性组织,也叫万维网联盟,标准包括HTML、CSS、ECMAScript等等,web标准的制定有很多好处,比如说:
- 可以统一开发流程,统一使用标准化开发工具(VSCode、WebStorm、Sublime),方便多人协作
- 学习成本降低,只需要学习标准就行,否则就要学习各个浏览器厂商标准
- 跨平台,方便迁移都不同设备
- 降低代码维护成本
6. 行级元素和块级元素分别有哪些及怎么转换?
常见的块级元素:p、div、form、ul、li、ol、table、h1、h2、h3、h4、h5、h6、dl、dt、dd
常见的行级元素:span、a、img、button、input、select
块级元素:
- 总是在新行上开始,就是每个块级元素独占一行,默认从上到下排列
- 宽度缺少时是它的容器的100%,除非设置一个宽度
- 高度、行高以及外边距和内边距都是可以设置的
- 块级元素可以容纳其它行级元素和块级元素
行内元素:
- 和其它元素都会在一行显示
- 高、行高以及外边距和内边距可以设置
- 宽度就是文字或者图片的宽度,不能改变
- 行级元素只能容纳文本或者其它行内元素
使用行内元素需要注意的是:
- 行内元素设置宽度width无效
- 行内元素设置height无效,但是可以通过line-height来设置
- 设置margin只有左右有效,上下无效
- 设置padding只有左右有效,上下无效
可以通过display属性对行内元素和块级元素进行切换(主要看第 2、3、4三个值):
7. H5有哪些新元素和新特性
HTML5主要是关于图像、位置、存储、多任务等功能的增加:
- 语义化标签,如:article、footer、header、nav等
- 视频video、音频audio
- 画布canvas
- 表单控件,calemdar、date、time、email
- 地理
- 本地离线存储,localStorage长期存储数据,浏览器关闭后数据不丢失,sessionStorage的数据在浏览器关闭后自动删除
- 拖拽释放
8. 如何使用HTML5中的Canvas元素绘制图形?
Canvas元素允许在网页上使用JavaScript绘制图形和动画。以下是一个简单的绘制矩形的示例:
<canvas id="myCanvas" width="200" height="200"></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
</script>
在这个示例中,使用 document.getElementById() 方法获取 Canvas 元素,并通过 getContext(“2d”) 获取2D绘图上下文。 然后,使用 fillStyle 属性设置填充颜色,fillRect() 方法绘制一个矩形。
9. cookie、sessionStorage 和 localStorage 的区别
cookie | localStorage | sessionStorage | |
---|---|---|---|
由谁初始化 | 客户端或服务器,服务器可以使用 Set-Cookie 请求头。 | 客户端 | 客户端 |
过期时间 | 手动设置 | 永不过期 | 当前页面关闭时 |
在当前浏览器会话(browser sessions)中是否保持不变 | 取决于是否设置了过期时间 | 是 | 否 |
是否随着每个 HTTP 请求发送给服务器 | 是,Cookies 会通过 Cookie 请求头,自动发送给服务器 | 否 | 否 |
容量(每个域名) | 4kb | 5MB | 5MB |
访问权限 | 任意窗口 | 任意窗口 | 当前页面窗口 |
10. script、script async 和 script defer 的区别
- <script> - HTML 解析中断,脚本被提取并立即执行。执行结束后,HTML 解析继续。
- <script async> - 脚本的提取、执行的过程与 HTML 解析过程并行,脚本执行完毕可能在 HTML 解析完毕之前。当脚本与页面上其他脚本独立时,可以使用 async,比如用作页面统计分析。
- <script defer> - 脚本仅提取过程与 HTML 解析过程并行,脚本的执行将在 HTML 解析完毕后进行。如果有多个含 defer 的脚本,脚本的执行顺序将按照在 document 中出现的位置,从上到下顺序执行。
注意:没有 src 属性的脚本,async 和 defer 属性会被忽略。
11. iframe的作用以及优缺点
优点
- 可以在页面上独立显示一个页面或者内容,不会与页面其他元素产生冲突。
- 可以在多个页面中重用同一个页面或者内容,可以减少代码的冗余。
- 加载是异步的,页面可以在不等待 iframe 加载完成的情况下进行展示。
- 方便地实现跨域访问
缺点
- 搜索引擎可能无法正确解析 iframe 中的内容
- 会阻塞主页面的 onload 事件
- 和主页面共享连接池,影响页面并行加载
12. Quirks(怪癖)模式是什么?它和Standards(标准)有什么区别?
页面如果写了DTD,就意味着这个页面采用对CSS支持更好的布局,而如果没有,则采用兼容之前的布局方式,这就是Quirks模式,有时候也叫怪癖模式、诡异模式、怪异模式。
区别:总体会有布局、样式解析、脚本执行三个方面区别,这里列举一些比较常见的区别:
- 盒模型:在W3C标准中,如果设置一个元素的宽度和高度,指的是元素内容的宽度和高度,然而在Quirks模式下,IE的宽度和高度还包含了padding和border
- 设置行内元素的高宽:在Standards模式下,给行内元素设置width和height都不会生效,而在Quriks模式下会生效
- 用margin:0 auto设置水平居中:在Standards模式下,设置margin:0 auto;可以使元素水平居中,但是在Quriks模式下失效
- 设置百分比高度:在Standards模式下,元素的高度是由包含的内容决定的,如果父元素没有设置百分比的高度,子元素设置百分比的高度是无效的
13. meta viewport 是做什么用的,怎么写?
Viewport,适配移动端,可以控制视口的大小和比例:
- <meta name=“viewport” content=“width=device-width, initial-scale=1, maximum-scale=1”>
其中,content 参数有以下几种:
- width viewport :宽度(数值/device-width)
- height viewport :高度(数值/device-height)
- initial-scale :初始缩放比例
- maximum-scale :最大缩放比例
- minimum-scale :最小缩放比例
- user-scalable :是否允许用户缩放(yes/no)
14.ES5和ES6的区别?
ES5(ECMAScript 5)和ES6(ECMAScript 2015)是JavaScript的两个主要版本,它们在发布时间、数据类型以及变量声明等方面存在区别。以下是具体分析:
- 发布时间
- ES5:于2009年推出。
- ES6:于2015年推出,也称为ECMAScript 2015。
- 数据类型
- ES5:支持原始数据类型,包括字符串、数字、布尔值、null和undefined。
- ES6:引入了新的原始数据类型symbol以支持唯一值。
- 变量声明
- ES5:只能使用var关键字定义变量。
- ES6:引入了let和const两种新的定义变量方式。
- 性能方面
- ES5:由于某些功能的缺失,其性能相对较低。
- ES6:具有新功能和速记存储实现,因而性能更高。
- 对象操作
- ES5:对象操作比较耗时。
- ES6:引入了解构和展开运算符,使对象操作更便捷。
- 函数声明
- ES5:使用function关键字定义函数。
- ES6:引入了箭头函数,简化了函数的语法,并改善了this绑定。
- 循环语句
- ES5:使用for循环遍历元素。
- ES6:引入了for…of循环,用于可迭代对象的值迭代。
- 代码转换
- ES5:完全被当前浏览器支持。
- ES6:可以使用Babel和Traceur等转译器将ES6代码转换为ES5代码以实现兼容性。
JS
1. js基本数据类型有哪些及它们的区别
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
2. 数据类型检测的方式有哪些
(1)typeof
javascript
复制代码console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中数组、对象、null都会被判断为object,其他判断都正确。
(2)instanceof
instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
javascript
复制代码console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
(3) constructor
javascript
复制代码console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:
javascript
复制代码function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:
javascript
复制代码var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
同样是检测对象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()做判断
javascript
复制代码Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
通过原型链做判断
javascript
复制代码obj.__proto__ === Array.prototype;
通过ES6的Array.isArray()做判断
javascript
复制代码Array.isArrray(obj);
通过instanceof做判断
javascript
复制代码obj instanceof Array
通过Array.prototype.isPrototypeOf
javascript
复制代码Array.prototype.isPrototypeOf(obj)
4. 请简述JavaScript中的this
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
- 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
- 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
- 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
- 第四种是== apply 、 call 和 bind 调用模式==,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
5. 箭头函数与普通函数有什么区别
(1)箭头函数与普通函数有什么区别
- 如果没有参数,就直接写一个空括号即可
- 如果只有一个参数,可以省去参数的括号
- 如果有多个参数,用逗号分割
- 如果函数体的返回值只有一句,可以省略大括号
- 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数:
javascript
复制代码let fn = () => void doesNotReturn();
(2)箭头函数没有自己的this
箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
(3)箭头函数继承来的this指向永远不会改变
javascript
复制代码var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
javascript
复制代码var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
(5)箭头函数不能作为构造函数使用
构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(6)箭头函数没有自己的arguments
箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
(7)箭头函数没有prototype
(8)箭头函数不能用作Generator函数,不能使用yeild关键字
6. AMD和CommonJS的区别
它们都是实现模块体系的方式,直到 ES2015 出现之前,JavaScript 一直没有模块体系。CommonJS 是同步的,而 AMD(Asynchronous Module Definition) 从全称中可以明显看出是异步的。CommonJS 的设计是为服务器端开发考虑的,而 AMD 支持异步加载模块,更适合浏览器。
我发现 AMD 的语法非常冗长,CommonJS 更接近其他语言 import 声明语句的用法习惯。大多数情况下,我认为 AMD 没有使用的必要,因为如果把所有 JavaScript 都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS 语法上更接近 Node 编写模块的风格,在前后端都使用 JavaScript 开发之间进行切换时,语境的切换开销较小。
我很高兴看到 ES2015 的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和 Node 中完全推出,但是我们可以使用代码转换工具进行转换。
7. ES6模块与CommonJS模块有什么异同?
ES6 Module和CommonJS模块的区别:
- CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
- import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。
ES6 Module和CommonJS模块的共同点:
- CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。
8. 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声明的变量是不允许改变指针的指向。
区别 | var | let | const |
---|---|---|---|
是否有块级作用域 | × | √ | √ |
是否存在变量提升 | √ | × | × |
是否添加全局属性 | √ | × | × |
能否重复声明变量 | √ | × | × |
是否存在暂时性死区 | × | √ | √ |
是否必须设置初始值 | × | × | √ |
能否改变指针指向 | √ | √ | × |
9.new操作符的实现原理
new操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
10. 数组有哪些原生方法?
数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。
数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。
数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法。
数组归并方法 reduce() 和 reduceRight() 方法。
11. for in和for of的区别
for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下
- for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
- for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
- 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;
总结: for…in 循环主要是为了遍历对象而生,不适用于遍历数组;for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
12. 数组的遍历方法有哪些?
方法 | 是否改变原数组 | 特点 |
---|---|---|
forEach() | 否 | 数组方法,值是基本类型, 改变不了;如果是引用类型分两种情况:1、没有修改形参元素的地址值, 只是修改形参元素内部的某些属性,会改变原数组;2、直接修改整个元素对象时,无法改变原数组,没有返回值 |
map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 |
filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
for…of | 否 | for…of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
13. forEach和map的区别
这方法都是用来遍历数组的,两者区别如下:
- forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
- map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
14. 原型和原型链
1. 对原型、原型链的理解
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
2. 原型修改、重写
javascript
复制代码function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false
可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:
javascript
复制代码Person.prototype = {
getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
3. 原型链指向
javascript
复制代码p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person
4. 原型链的终点是什么?如何打印出原型链的终点?
由于Object是构造函数,原型链终点是Object.prototype.proto,而Object.prototype.proto=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.proto。
5. 如何获得对象非原型链上的属性?
使用后**hasOwnProperty()**方法来判断属性是否属于原型链的属性:
javascript
复制代码function iterate(obj){
var res=[];
for(var key in obj){
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}
15. 对执行上下文,作用域(链),闭包的理解
1. 对闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
javascript
复制代码function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题。
javascript
复制代码for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:
- 第一种是使用闭包的方式
javascript
复制代码for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i)}
在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
- 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
javascript
复制代码for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
- 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式。
javascript
复制代码for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
2. 对作用域、作用域链的理解
1)全局作用域和函数作用域
(1)全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
(2)函数作用域
- 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
- 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
- let和const声明的变量不会有变量提升,也不可以重复声明
- 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
3. 对执行上下文的理解
1). 执行上下文类型
(1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
(2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
(3) eval函数执行上下文
执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。
2). 执行上下文栈
- JavaScript引擎使用执行上下文栈来管理执行上下文
- 当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
javascript
复制代码let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()
3). 创建执行上下文
创建执行上下文有两个阶段:创建阶段和执行阶段
1)创建阶段
(1)this绑定
- 在全局执行上下文中,this指向全局对象(window对象)
- 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined
(2)创建词法环境组件
- 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
- 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域
(3)创建变量环境组件
- 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
2)执行阶段 此阶段会完成对变量的分配,最后执行完代码。
简单来说执行上下文就是指:
在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
16. 实现call、apply 及 bind 函数
(1)call 函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
- 返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
(2)apply 函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性
- 返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
(3)bind 函数的实现步骤
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
17. call、apply 函数的区别
它们的作用一模一样,区别仅在于传入参数的形式的不同。
- apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
- call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
18. 异步编程的实现方式?
JavaScript中的异步机制可以分为以下几种:
- **回调函数** 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- **Promise** 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- **generator** 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- **async 函数** 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
19. setTimeout、Promise、Async/Await 的区别
(1)setTimeout
javascript
复制代码console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
(2)Promise
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
javascript 代码解读复制代码
javascript
复制代码console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时:
- promise1.then() 的回调就是一个 task
- promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
- promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
- setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
(3)async/await
javascript
复制代码async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
例如:
javascript
复制代码async function func1() {
return 1
}
console.log(func1())
func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。
javascript
复制代码func1().then(res => {
console.log(res); // 30
})
await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
20. Promise.all和Promise.race的区别的使用场景
(1)Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
(2)Promise.race
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
javascript
复制代码Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
21. 对async/await 的理解
async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:
async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)
所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:
async function testAsy(){
return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
console.log(v) // hello world
})
那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
注意: Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
22. 浏览器的垃圾回收机制
(1)垃圾回收的概念
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
(2)垃圾回收的方式
浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。
1)标记清除
- 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工��,销毁那些带标记的值,并回收他们所占用的内存空间。
2)引用计数
- 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
- 这种方法会引起循环引用的问题:例如:
obj1
和obj2
通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1
和obj2
还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
这种情况下,就要手动释放变量占用的内存:
obj1.a = null
obj2.a = null
(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。- 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
23. 哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
24. ES6有哪些新特性
- 箭头函数
- 解构赋值
- 模板字符串
- promise
- symbol Symbol是ES6中引入的一种新的基本数据类型,用于表示一个独一无二的值,不能与其他数据类型进行运算
- 新的变量声明方式-let和const
- 模块化-es6新增了模块化,根据功能封装模块,通过import导入,然后通过export导出也可以使用export default导出
- for…of 循环,用于遍历可迭代对象(如数组、Map 和 Set)中的元素
- 扩展运算符:使用 … 可以将数组或对象展开成多个参数,或者将多个参数合并成一个数组
- 展开运算符:在ES6中用…来表示展开运算符,它可以将数组或者对象进行展开
- Map 和 Set,引入了两种新的数据结构,分别用于存储键值对和唯一值
- Proxy,允许在对象和函数调用等操作前后添加自定义的行为
- 类(Class),引入了面向对象编程中类的概念
- 默认参数(Default Parameter),在定义函数时可以给参数设置默认值
25. 匿名函数的典型应用场景是什么?
匿名函数可以在 IIFE 中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。
(function () {
// 一些代码。
})();
匿名函数可以作为只用一次,不需要在其他地方使用的回调函数。当处理函数在调用它们的程序内部被定义时,代码具有更好地自闭性和可读性,可以省去寻找该处理函数的函数体位置的麻烦。
setTimeout(function () {
console.log('Hello world!');
}, 1000);
匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
26.你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处
柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被 curry 的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。
function curry(fn) {
if (fn.length === 0) {
return fn;
}
function _curried(depth, args) {
return function (newArgument) {
if (depth - 1 === 0) {
return fn(...args, newArgument);
}
return _curried(depth - 1, [...args, newArgument]);
};
}
return _curried(fn.length, []);
}
function add(a, b) {
return a + b;
}
var curriedAdd = curry(add);
var addFive = curriedAdd(5);
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
27.什么是事件循环?调用堆栈和任务队列之间有什么区别?
事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。
28.js设计模式有哪些?
总体来说设计模式分为三大类:(C5S7B11)
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式/发布订阅模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
手写单例模式(创建模式)
javascript
复制代码let CreateSingleton = (function(){
let instance;
return function(name) {
if (instance) {
return instance;
}
this.name = name;
return instance = this;
}
})();
CreateSingleton.prototype.getName = function() {
console.log(this.name);
}
代码测试
javascript
复制代码let Winner = new CreateSingleton('Winner');
let Looser = new CreateSingleton('Looser');
console.log(Winner === Looser); // true
console.log(Winner.getName()); // 'Winner'
console.log(Looser.getName()); // 'Winner'
手写观察者模式(行为模式)
javascript
复制代码// 定义observe
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// notify
queuedObservers.forEach(observer => observer());
return result;
}
});
代码测试
javascript
复制代码
obj = observable({
name:'789'
})
observe(function test(){
console.log('触发了')
})
obj.name ="前端柒八九"
// 触发了
// 前端柒八九
手写发布订阅 (行为模式)
javascript
复制代码class Observer {
caches = {}; // 事件中心
// eventName事件名-独一无二, fn订阅后执行的自定义行为
on (eventName, fn){
this.caches[eventName] = this.caches[eventName] || [];
this.caches[eventName].push(fn);
}
// 发布 => 将订阅的事件进行统一执行
emit (eventName, data) {
if (this.caches[eventName]) {
this.caches[eventName]
.forEach(fn => fn(data));
}
}
// 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
off (eventName, fn) {
if (this.caches[eventName]) {
const newCaches = fn
? this.caches[eventName].filter(e => e !== fn)
: [];
this.caches[eventName] = newCaches;
}
}
}
代码测试
javascript
复制代码ob = new Observer();
l1 = (data) => console.log(`l1_${data}`)
l2 = (data) => console.log(`l2_${data}`)
ob.on('event1',l1)
ob.on('event1',l2)
//发布订阅
ob.emit('event1',789)
// l1_789
// l2_789
// 取消,订阅l1
ob.off('event1',l1)
ob.emit('event1',567)
//l2_567
观察者模式 VS 发布订阅模式
- 从表面上看:
- 观察者模式里,只有两个角色 —— 观察者 + 被观察者
- 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— {经纪人|Broker}
- 往更深层次讲:
- 观察者和被观察者,是松耦合的关系
- 发布者和订阅者,则完全不存在耦合
- 从使用层面上讲:
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种{跨应用的模式|cross-application pattern} ,比如我们常用的消息中间件
Vue
1. vue的生命周期有哪些及每个生命周期做了什么?
beforeCreate是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。
created在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。
beforeMount发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。
mounted在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
beforeUpdate发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
updated发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
beforeDestroy发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
2. vue响应式原理是什么?vue3的响应式有何不同
Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
3. vue3和vue2的区别
- 源码组织方式变化:使用 TS 重写
- 支持 Composition API:基于函数的API,更加灵活组织组件逻辑(vue2用的是options api)
- 响应式系统提升:Vue3中响应式数据原理改成proxy,可监听动态新增删除属性,以及数组变化
- 编译优化:vue2通过标记静态根节点优化diff,Vue3 标记和提升所有静态根节点,diff的时候只需要对比动态节点内容
- 打包体积优化:移除了一些不常用的api(inline-template、filter)
- 生命周期的变化:使用setup代替了之前的beforeCreate和created
- Vue3 的 template 模板支持多个根标签
- Vuex状态管理:创建实例的方式改变,Vue2为new Store , Vue3为createStore
- Route 获取页面实例与路由信息:vue2通过this获取router实例,vue3通过使用 getCurrentInstance/ userRoute和userRouter方法获取当前组件实例
- Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props
- 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下
4. 谈一谈对 MVVM 的理解?
MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
5. 在 Vue2.x 中如何检测数组的变化?
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
6. v-model 双向绑定的原理是什么?
v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。 可以通过model属性的prop和event属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性 。
7.vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?
简单来说,diff算法有以下过程
- 同级比较,再比较子节点
- 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较都有子节点的情况(核心diff)
- 递归比较子节点
正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x借鉴了 ivi算法和 inferno算法
在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)
该算法中还运用了动态规划的思想求解最长递归子序列。
8. vue组件通信方式有哪些及原理
父子组件通信
父->子props,子->父**$on**、$emit
获取父子组件实例 $parent、$children
Ref 获取实例的方式调用组件的属性或者方法
Provide、inject 官方不推荐使用,但是写组件库时很常用
兄弟组件通信
Event Bus` 实现跨组件通信 `Vue.prototype.$bus = new Vue
Vuex
跨级组件通信
Vuex
$attrs、$listeners
9. Vue的路由实现, hash路由和history路由实现原理说一下
`location.hash`的值实际就是URL中`#`后面的东西。
history实际采用了HTML5中提供的API来实现,主要有history.pushState()和history.replaceState()。
10. 说一下 v-if 与 v-show 的区别
当条件不成立时,v-if不会渲染DOM元素,v-show操作的是样式(display),切换当前DOM的显示和隐藏。
11. keep-alive的常用属性有哪些及实现原理
keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
常用的两个属性include/exclude,允许组件有条件的进行缓存。
两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态。
keep-alive的中还运用了LRU(Least Recently Used)算法。
12. nextTick 的作用是什么?他的实现原理是什么?
在下次 DOM 更新循环结束之后执行延迟回调。nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用
- Promise
- MutationObserver
- setImmediate
- 如果以上都不行则采用setTimeout
定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
13. 说一下 Vue SSR 的实现原理
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。
SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。
14. Vue 组件的 data 为什么必须是函数
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
15. 说一下 Vue 的 computed 的实现原理
当组件实例触发生命周期函数 beforeCreate 后,它会做一系列事情,其中就包括对 computed 的处理。
它会遍历 computed 配置中的所有属性,为每一个属性创建一个 Watcher 对象,并传入一个函数,该函数的本质其实就是 computed 配置中的 getter,这样一来,getter 运行过程中就会收集依赖
但是和渲染函数不同,为计算属性创建的 Watcher 不会立即执行,因为要考虑到该计算属性是否会被渲染函数使用,如果没有使用,就不会得到执行。因此,在创建 Watcher 的时候,它使用了 lazy 配置,lazy 配置可以让 Watcher 不会立即执行。
收到 lazy 的影响,Watcher 内部会保存两个关键属性来实现缓存,一个是 value,一个是 dirty
value 属性用于保存 Watcher 运行的结果,受 lazy 的影响,该值在最开始是 undefined
dirty 属性用于指示当前的 value 是否已经过时了,即是否为脏值,受 lazy 的影响,该值在最开始是 true
Watcher 创建好后,vue 会使用代理模式,将计算属性挂载到组件实例中
当读取计算属性时,vue 检查其对应的 Watcher 是否是脏值,如果是,则运行函数,计算依赖,并得到对应的值,保存在 Watcher 的 value 中,然后设置 dirty 为 false,然后返回。
如果 dirty 为 false,则直接返回 watcher 的 value
巧妙的是,在依赖收集时,被依赖的数据不仅会收集到计算属性的 Watcher,还会收集到组件的 Watcher
当计算属性的依赖变化时,会先触发计算属性的 Watcher 执行,此时,它只需设置 dirty 为 true 即可,不做任何处理。
由于依赖同时会收集到组件的 Watcher,因此组件会重新渲染,而重新渲染时又读取到了计算属性,由于计算属性目前已为 dirty,因此会重新运行 getter 进行运算
而对于计算属性的 setter,则极其简单,当设置计算属性时,直接运行 setter 即可。
16. 说一下 Vue complier 的实现原理是什么样的?
在使用 vue 的时候,我们有两种方式来创建我们的 HTML 页面,第一种情况,也是大多情况下,我们会使用模板 template 的方式,因为这更易读易懂也是官方推荐的方法;第二种情况是使用 render 函数来生成 HTML,它比 template 更接近最终结果。
complier 的主要作用是解析模板,生成渲染模板的 render, 而 render 的作用主要是为了生成 VNode
complier 主要分为 3 大块:
- parse:接受 template 原始模板,按着模板的节点和数据生成对应的 ast
- optimize:遍历 ast 的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,通过 diff 减少去对比这部分DOM,提升性能
- generate 把前两步生成完善的 ast,组成 render 字符串,然后将 render 字符串通过 new Function 的方式转换成渲染函数
17. Vue 与 Angular 以及 React 的区别是什么?
React 和 Vue 有许多相似之处,它们都有:
- 使用 Virtual DOM
- 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
- 将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
由于有着众多的相似处,我们会用更多的时间在这一块进行比较。这里我们不只保证技术内容的准确性,同时也兼顾了平衡的考量。我们需要承认 React 比 Vue 更好的地方,比如更丰富的生态系统。
AngularJS (Angular 1)
Vue 的一些语法和 AngularJS 的很相似 (例如 v-if vs ng-if)。因为 AngularJS 是 Vue 早期开发的灵感来源。然而,AngularJS 中存在的许多问题,在 Vue 中已经得到解决。
18. 说一下 watch 与 computed 的区别是什么?以及他们的使用场景分别是什么?
区别:
- 都是观察数据变化的(相同)
- 计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;
- computed 有缓存,它依赖的值变了才会重新计算,watch 没有;
- watch 支持异步,computed 不支持;
- watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)
- watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;
- computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)
watch 的 参数:
- deep:深度监听
- immediate :组件加载立即触发回调函数执行
computed 缓存原理:
conputed本质是一个惰性的观察者;当计算数据存在于 data 或者 props里时会被警告;
vue 初次运行会对 computed 属性做初始化处理(initComputed),初始化的时候会对每一个 computed 属性用 watcher 包装起来 ,这里面会生成一个 dirty 属性值为 true;然后执行 defineComputed 函数来计算,计算之后会将 dirty 值变为 false,这里会根据 dirty 值来判断是否需要重新计算;如果属性依赖的数据发生变化,computed 的 watcher 会把 dirty 变为 true,这样就会重新计算 computed 属性的值。
19.说一下你知道的 vue 修饰符都有哪些?
在 vue 中修饰符可以分为 3 类:
- 事件修饰符
- 按键修饰符
- 表单修饰符
事件修饰符
在事件处理程序中调用 event.preventDefault 或 event.stopPropagation 方法是非常常见的需求。尽管可以在 methods 中轻松实现这点,但更好的方式是:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,vue 为 v-on 提供了事件修饰符。通过由点 . 表示的指令后缀来调用修饰符。
常见的事件修饰符如下:
- .stop:阻止冒泡。
- .prevent:阻止默认事件。
- .capture:使用事件捕获模式。
- .self:只在当前元素本身触发。
- .once:只触发一次。
- .passive:默认行为将会立即触发。
按键修饰符
除了事件修饰符以外,在 vue 中还提供了有鼠标修饰符,键值修饰符,系统修饰符等功能。
- .left:左键
- .right:右键
- .middle:滚轮
- .enter:回车
- .tab:制表键
- .delete:捕获 “删除” 和 “退格” 键
- .esc:返回
- .space:空格
- .up:上
- .down:下
- .left:左
- .right:右
- .ctrl:ctrl 键
- .alt:alt 键
- .shift:shift 键
- .meta:meta 键
表单修饰符
vue 同样也为表单控件也提供了修饰符,常见的有 .lazy、 .number 和 .trim。
- .lazy:在文本框失去焦点时才会渲染
- .number:将文本框中所输入的内容转换为number类型
- .trim:可以自动过滤输入首尾的空格
20. 如何实现 vue 项目中的性能优化?
编码阶段
- 尽量减少 data 中的数据,data 中的数据都会增> - 加 getter 和 setter,会收集对应的 watcher
- v-if 和 v-for 不能连用
- 如果需要使用 v-for 给每项元素绑定事件时使用事件代理
- SPA 页面采用 keep-alive 缓存组件
- 在更多的情况下,使用 v-if 替代 v-show
- key 保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
SEO 优化
- 预渲染
- 服务端渲染 SSR
打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用 cdn 加载第三方模块
- 多线程打包 happypack
- splitChunks 抽离公共文件
- sourceMap 优化
- 用户体验
用户体验
- 骨架屏
- PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。
21. vue 中的 spa 应用如何优化首屏加载速度?
优化首屏加载可以从这几个方面开始:
- 请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
- 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
- gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
- http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
- 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
- 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
- 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
- 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
- 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
- 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
- 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
- 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
- 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片
22. Vue 中的 Key 的作用是什么?
key 的作用主要是为了高效的更新虚拟 DOM 。另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
其实不只是 vue,react 中在执行列表渲染时也会要求给每个组件添加上 key 这个属性。
要解释 key 的作用,不得不先介绍一下虚拟 DOM 的 Diff 算法了。
我们知道,vue 和 react 都实现了一套虚拟 DOM,使我们可以不直接操作 DOM 元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的 Diff 算法。
vue 和 react 的虚拟 DOM 的 Diff 算法大致相同,其核心有以下两点:
- 两个相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构。
- 同一层级的一组节点,他们可以通过唯一的 id 进行区分。
基于以上这两点,使得虚拟 DOM 的 Diff 算法的复杂度从 O(n^3) 降到了 O(n) 。
当页面的数据发生变化时,Diff 算法只会比较同一层级的节点:
- 如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。
- 如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。
比如一下这个情况:
我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:
即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E
是不是很没有效率?
所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
23. 组件中写 name 选项有哪些好处
- 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )
- 可以通过 name 属性实现缓存功能(keep - alive)
可以通过 name 来识别组件(跨级组件通信时非常重要)- 使用 vue-devtools 调试工具里显示的组见名称是由 vue 中组件 name 决定的
24. 说一下 ref 的作用是什么?
ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:
-如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素
- 如果用在子组件上,引用就指向组件实例
所以常见的使用场景有:
- 基本用法,本页面获取 DOM 元素
- 获取子组件中的 data
- 调用子组件中的方法
25. 你的接口请求一般放在哪个生命周期中?为什么要这样做?
接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:能更快获取到服务端数据,减少页面
- loading 时间
- SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
- created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题
CSS
1.CSS盒模型,在不同浏览器的差异
css 标准盒子模型
css盒子模型 又称为框模型(Box Model),包含了元素内容(content)、内边距(padding)、边框(border)、外边距(margin)几个要素。如下图:
图中的内层是content依次是padding border margin。通常我们设置背景时就是内容、内边距、边框这三部分,如果border设置颜色的时候会显示boder颜色当boder颜色是透明时会显示background-color的颜色。而该元素的子元素的是从content开始的。而外边距是透明的,不会遮挡其他元素
元素框的总宽度=width+padding-left+padding-right+border-left+border-right+margin-left+margin-right;
元素框的总高度=height+padding-top+padding-bottom+border-top+border-bottom+margin-top+margin-bottom;
IE盒子模型
IE盒子模型如下图:
图中的内层是content依次是content padding border。通常我们设置背景时就是内容、内边距、边框这三部分。而外边距是透明的,不会遮挡其他元素。
元素框的总宽度=width(padding-left+padding-right+border-left+border-right);
元素框的总高度=height(padding-top+padding-bottom+border-top+border-bottom;
两个模型宽度和高度的计算(是不一样的)
w3c中的盒子模型的宽:包括margin+border+padding+width; width:margin2+border2+padding2+width; height:margin2+border2+padding2+height;
iE中的盒子模型的width:包括border+padding+width;
上面的两个宽度相加的属性是一样的。 因此我们应该选择标准盒子模型,在网页的顶部加上 DOCTYPE 声明。
2. CSS所有选择器及其优先级、使用场景,哪些可以继承,如何运用at规则
css选择器种类有:
- 通用选择器:*
- id选择器:#header{}
- class选择器:.header{}
- 元素选择器:div{}
- 子选择器:ul > li{}
- 后代选择器:div p{}
- 伪类选择器::hover、::selection、.action、:first-child、:last-child、:first-of-type、:last-of-type、:nth-of-type(n)、:nth-of-last-type(n)等,例如a:hover{}
- 伪元素选择器: :after、:before等,例如:li:after
- 属性选择器: input[type=“text”]
- 组合选择器:E,F/E F(后代选择器)/E>F(子元素选择器)/E+F(直接相邻元素选择器----匹配之后的相邻同级元素)/E~F(普通相邻元素选择器----匹配之后的同级元素)
- 层次选择器:p~ul 选择前面有p元素的每个ul元素
css选择器优先级:
选择器优先级由高到低分别为:
!important > 作为style属性写在元素标签上的内联样式 > id选择器>类选择器>伪类选择器>属性选择器>标签选择器> 通配符选择器(* 应少用)>浏览器自定义;当比较多个相同级别的CSS选择器优先级时,它们定义的位置将决定一切。下面从位置上将CSS优先级由高到低分为六级:
1、位于标签里的中所定义的CSS拥有最高级的优先权。
2、第二级的优先属性由位于 标签中的 @import 引入样式表所定义。
3、第三级的优先属性由标签所引入的样式表定义。
4、第四级的优先属性由标签所引入的样式表内的 @import 导入样式表定义。
5、第五级优先的样式有用户设定。
6、最低级的优先权由浏览器默认。使用场景:
- class使用场景:需要某些特定样式的标签则放在同一个class中,需要此样式的标签可再添加此类。(class不可被javascript中的GetElementByID函数所调用)
- id使用场景:1、根据提供的唯一id号快速获取标签对象,如:document.getElementById(id) ;2、用于充当label标签for属性的值:示例:用户名:,表示单击此label标签时,id为userid的标签获得焦点
CSS哪些属性可以继承?
css继承特性主要是指文本方面的继承(比如字体、颜色、字体大小等),盒模型相关的属性基本没有继承特性。
- 不可继承的:
display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、top、bottom、left、right、z-index、float、clear、 table-layout、vertical-align、page-break-after、page-bread-before和unicode-bidi。- 所有元素可继承的:
visibility和cursor- 终极块级元素可继承的:
text-indent和text-align- 内联元素可继承的:
letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction- 列表元素可继承的:
list-style、list-style-type、list-style-position、list-style-image
常用at规则及使用示例:
- @charset
- @import
- @namespace
- @document
- @font-face
- @keyframes
- @media
- @page
- @supports
- /定义字符集/
@charset “utf-8”- /导入css文件/
@import “base.css”- /自定义字体/
@font-face {}- /声明CSS3 animation动画关键帧/
@keyframes fadeIn {}- /媒体查询/
@media{}
3. CSS伪类和伪元素有哪些,它们的区别和实际应用
伪类的例子有:
- :hover
- :active
- :first-child
- :visited
- 等。
伪元素的例子有:
- :first-line
- :first-letter
- :after
- :before
伪类和伪元素的根本区别在于:
它们是否创造了新的元素(抽象)。从我们模仿其意义的角度来看,如果需要添加新元素加以标识的,就是伪元素,反之,如果只需要在既有元素上添加类别的,就是伪类。
伪元素在一个选择器里只能出现一次,并且只能出现在末尾;
伪类则是像真正的类一样发挥着类的作用,没有数量上的限制,只要不是相互排斥的伪类,也可以同时使用在相同的元素上。
实际使用:
伪类用一个冒号表示 :first-child
伪元素则使用两个冒号表示 ::first-line
4. CSS几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理
1)、static定位(普通流定位) -------------- 默认定位
2)、float定位(浮动定位) 例:float:left;
有两个取值:left(左浮动)和right(右浮动)。浮动元素会在没有浮动元素的上方,效果上看是遮挡住了没有浮动的元素,有float样式规则的元素是脱离文档流的,它的父元素的高度并不能有它撑开。
3)、relative定位(相对定位) position:relative;
相对本元素的左上角进行定位,top,left,bottom,right都可以有值。虽然经过定位后,位置可能会移动,但是本元素并没有脱离文档流,还占有原来的页面空间。可以设置z-index。使本元素相对于文档流中的元素,或者脱离文档流但是z-index的值比本元素的值要小的元素更加靠近用户的视线。
相对定位最大的作用是为了实现某个元素相对于本元素的左上角绝对定位,本元素需要设置position为relative。
4)、absolute定位(绝对定位) position:absolute;
相对于祖代中有relative(相对定位)并且离本元素层级关系上是最近的元素的左上角进行定位,如果在祖代元素中没有有relative定位的,就默认相对于body进行定位。
绝对定位是脱离文档流的,与浮动定位是一样的效果,会压在非定位元素的上方。可以设置z-index属性。
雪碧图实现原理:
CSS雪碧的基本原理是把你的网站上用到的一些图片整合到一张单独的图片中,从而减少你的网站的HTTP请求数量。该图片使用CSS background和background-position属性渲染,这也就意味着你的标签变得更加复杂了,图片是在CSS中定义,而非标签。
5. 写出尽可能多的水平垂直居中的方案并对比它们的优缺点
行内元素水平居中:
首先看它的父元素是不是块级元素,如果是,则直接给父元素设置 text-align: center;
如果不是,则先将其父元素设置为块级元素,再给父元素设置 text-align: center;
块级元素水平居中(定宽度):
1)需要谁居中,给其设置 margin: 0 auto; (作用:使盒子自己居中)
2) 首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的left:50%,即让子元素的左上角水平居中;
设置绝对子元素的 margin-left: -元素宽度的一半px; 或者设置transform: translateX(-50%);块级元素水平居中(不宽度):
- 默认子元素的宽度和父元素一样,这时需要设置子元素为display: inline-block; 或 display: inline;即将其转换成行内块级/行内元素,给父元素设置 text-align: center;
- 首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的left:50%,即让子元素的左上角水平居中;
利用css3新增属性transform: translateX(-50%);使用flexbox布局实现水平居中(宽度定不定都可以):
使用flexbox布局,只需要给待处理的块状元素的父元素添加属性 display: flex; justify-content: center;
单行的行内元素垂直居中:
只需要设置单行行内元素的"行高等于盒子的高"即可;
多行的行内元素垂直居中:
使用给父元素设置**display:table-cell;和vertical-align: middle;**属即可;
块级元素垂直居中方法一:使用定位
首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的top: 50%,即让子元素的左上角垂直居中
定高度:设置绝对子元素的 margin-top: -元素高度的一半px; 或者设置transform: translateY(-50%);
不定高度:利用css3新增属性transform: translateY(-50%);
块级元素垂直居中****方法二:使用flexbox布局实现(高度定不定都可以)
使用flexbox布局,只需要给待处理的块状元素的父元素添加属性 display: flex; align-items: center;
水平垂直居中-已知高度和宽度的元素:
方法一:
设置父元素为相对定位,给子元素设置绝对定位,top: 0; right: 0; bottom: 0; left: 0; margin: auto;
方法二:
设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; margin-left: --元素宽度的一半px; margin-top: --元素高度的一半px;
水平垂直居中-未知高度和宽度的元素:
方法一:使用定位属性
设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%);
方案二:使用flex布局实现
设置父元素为flex定位,justify-content: center; align-items: center;
- 本文主要摘自各博主的面试题汇总。
- 主要来源:稀土掘金博主程序员吴铭
- 本文仅作个人学习笔记使用,无商业用途。
- 若不经意侵犯原文作者著作权,请联系本人。
- 如若转载,请先声明。