前端面试题笔记
js数据类型
-
基本数据类型(栈)
Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型。
-
引用数据类型(堆)
引用数据类型统称为 Object 对象,主要包括对象、数组、函数、日期和正则等等。
Doctype作用?
-
Doctype声明于文档最前面,告诉浏览器以何种方式来渲染页面,这里有两种模式,严格模式和混杂模式。
-
严格模式的排版和 JS 运作模式是 以该浏览器支持的最高标准运行。
-
混杂模式,向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。
var、let、const的区别
- var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
- let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
- const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
前端优化网站性能
-
减少HTTP请求数量
在浏览器与服务器进行通信时,主要是通过 HTTP 进行通信。浏览器与服务器需要经过三次握手,每次握手需要花费大量时间。- CSS Sprites
将多张图片合并成一张图片达到减少HTTP请求 - 合并 CSS 和 JS 文件
现在前端有很多工程化打包工具,如:grunt、gulp、webpack等。为了减少 HTTP 请求数量,可以通过这些工具再发布前将多个CSS或者多个JS合并成一个文件。 - 采用 lazyLoad
俗称懒加载,可以控制网页上的内容在一开始无需加载,不需要发请求,等到用户操作真正需要的时候立即加载出内容。
- CSS Sprites
-
控制资源文件加载优先级
浏览器在加载HTML内容时,是将HTML内容从上至下依次解析,解析到link或者script标签就会加载href或者src对应链接内容,为了第一时间展示页面给用户,就需要将CSS提前加载,不要受 JS 加载影响。
一般情况下都是CSS在头部,JS在底部。
-
利用浏览器缓存
浏览器缓存是将网络资源存储在本地,等待下次请求该资源时,如果资源已经存在就不需要到服务器重新请求该资源,直接在本地读取该资源。 -
减少重排(Reflow)基本原理:重排是DOM的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树中受到影响的部分失效,浏览器会验证 DOM 树上的所有其它结点的visibility属性,这也是Reflow低效的原因。如果Reflow的过于频繁,CPU使用率就会急剧上升。
减少Reflow,如果需要在DOM操作时添加样式,尽量使用 增加class属性,而不是通过style操作样式。
-
减少 DOM 操作
-
图标使用 IconFont 替换
网页从输入网址到渲染完成经历的过程
大致可以分为如下7步:
- 输入网址;
- 发送到DNS服务器,并获取域名对应的web服务器对应的ip地址;
- 与web服务器建立TCP连接;
- 浏览器向web服务器发送http请求;
- web服务器响应请求,并返回指定url的数据(或错误信息,或重定向的新的url地址);
- 浏览器下载web服务器返回的数据及解析html源文件;
- 生成DOM树,解析css和js,渲染页面,直至显示完成;
5种常见的状态码
- 200 (ok) : 请求已成功, 请求所希望的响应头或数据体将随此响应返回。
- 303 (See Other) : 告知客户端使用另一个URL来获取资源。
- 400 (Bad Request) : 请求格式错误. 1)语义有误; 2)请求参数有误。
- 404 (Not Found) : 请求失败,服务器上无法找到请求的资源。
- 500 (Internal Server Error) : 服务器错误。
js的运行机制
-
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
js 深浅拷贝
深浅拷贝的定义
-
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
-
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
赋值 浅拷贝 深拷贝的区别
-
赋值:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
-
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
-
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。
代码演示
-
赋值
var ta = { name: '张三', hobby: ['学习', '跑步'] } var tb = ta tb.name = '李四' tb.hobby[0] = '打篮球' console.log(ta.name + ' ' + ta.hobby) console.log(tb.name + ' ' + tb.hobby) // 结果 //李四 打篮球,跑步 //李四 打篮球,跑步
结论:赋值是相互影响的
-
浅拷贝
var ta = {
name: '张三',
hobby: ['学习', '跑步']
}
function shallowCopy(obj){
var target = {}
for(var i in obj){
if(obj.hasOwnProperty(i)){
target[i] = obj[i]
}
}
return target
}
var tb = shallowCopy(ta)
tb.name = '李四'
tb.hobby[0] = '打篮球'
console.log(ta.name + ' ' + ta.hobby)
console.log(tb.name + ' ' + tb.hobby)
//结果
//张三 打篮球,跑步
//李四 打篮球,跑步
结论:基本类型互不影响,引用类型相互影响
- 深拷贝
var ta = {
name: '张三',
hobby: ['学习', '跑步']
}
function deepClone(obj){
var cloneObj = new obj.constructor()
if(typeof obj !== 'object') return obj
for(var i in obj){
if(obj.hasOwnProperty(i)){
cloneObj[i] = deepClone(obj[i])
}
}
return cloneObj
}
var tb = deepClone(ta)
tb.name = '李四'
tb.hobby[0] = '打篮球'
console.log(ta.name + ' ' + ta.hobby)
console.log(tb.name + ' ' + tb.hobby)
// 结果
//张三 学习,跑步
//李四 打篮球,跑步
结论:数据互不影响
防抖函数
定义
函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。
(在设定的时间内,又一次触发了事件,重新开始延时,代表的就是重新开始定时器)
(那么意味着上一次还没有结束的定时器要清除掉,重新开始)
代码演示
<body>
<!-- 需求:监听keyup事件,停止输入的一秒后把内容输出 -->
<input type="text" id="input">
<script>
var input = document.getElementById('input')
// 防抖的函数
// 利用闭包让timer一直存储在内存当中
function debounce(delay){
let timer
return function(value){
clearTimeout(timer)
timer = setTimeout(function (){
console.log(value)
}, delay)
}
}
var debounceFunc = debounce(1000)
input.addEventListener('keyup',function (e){
debounceFunc(e.target.value)
})
</script>
</body>
节流函数
定义
当持续触发事件的时候,保证一段时间内,只调用一次事件处理函数。
(举例:表单的提交,n秒内多次提交只有一次生效)
代码演示
<button id="btn">按钮</button>
<script>
// 需求:按钮的点击事件,两秒内只允许触发一次
function throttle(func, wait){
let timerOut
return function (){
if(!timerOut){
timerOut = setTimeout(function(){
func()
timerOut = null
}, wait)
}
}
}
function handle(){
console.log("点击了按钮")
}
document.getElementById("btn").onclick = throttle(handle, 2000)
</script>
作用域的预编译
函数作用域预编译
-
创建AO对象AO{}
-
找形参和变量声明将变量和形参名当做AO对象的属性名值为undefined
-
实参形参相统一
-
在函数体里面找函数声明值赋予函数体
全局作用域的预编译
- 创建GO对象
- 找变量声明将变量名作为GO对象的属性名值是undefined
- 找函数声明值赋予函数体
例题
function fn(a, c){
console.log(a)
var a = 123
console.log(a)
console.log(c)
function a(){ }
if(false){
var d = 678
}
console.log(d)
console.log(b)
var b = function(){ }
console.log(b)
function c(){ }
console.log(c)
}
fn(1,2)
// 预编译
// AO{
// a: undefined 1 function a(){}
// c: undefined 2 function c(){}
// d: undefined
// b: undefined
// }
ƒ a(){ }
123
ƒ c(){ }
undefined
undefined
ƒ (){ }
ƒ c(){ }
哪些操作会造成内存泄漏
- 闭包
- 意外的全局变量(如let a = b =1,其中的b就是意外的全局变量)
- 未清除的定时器
- 脱离dom的引用
什么是高阶函数
- 将函数作为参数或者返回值的函数
function highOrder(params, callback){
return callback(params)
}
手写map函数
- map是一个常用对数组的操作,它用于把数组的元素按照一定条件的处理,然后返回处理后的数组,生成新的数组。
var arr = [1, 2, 3]
var array = arr.map((item, index) =>{
return item * 2
})
console.log(array) // [2, 4, 6]
// 手写map
Array.prototype._map = function(callback) {
if (!Array.isArray(this) || !this.length || typeof callback !== 'function'){
return []} else{
let result = [];
let len = this.length;
for (let i = 0; i < len; i++)
{
result.push(callback(this[i], i, this));
}
return result;
}
}
var res = arr._map((item) => { return item * 2 });
console.log(res) // [2, 4, 6]
手写filter
- filter是一个常用对数组的操作,它用于把数组的某些元素过滤掉,然后返回剩下的元素。
var arr = [1, 2, 3]
var array = arr.filter(item => {
return item > 2
})
console.log(array) // [3]
// 手写filter
Array.prototype._filter = function(callback){
if(!Array.isArray(this) || !this.length || typeof callback !== 'function'){
return []
} else {
let result = [];
let len = this.length;
for(let i=0; i<len; i++){
if(callback(this[i], i, this)){
result.push(this[i]);
}
}
return result
}
}
var res = arr._filter((item) => { return item > 2 })
console.log(res) // [3]
手写reduce函数
- 函数用于把数组或对象归结为一个值,并返回这个值,使用方法为arr.reduct(func,memo),其中func为处理函数,memo为初始值,初始值可缺省。
var arr = [1, 2, 3] var num = arr.reduce((pre, cur) => { return pre + cur }, 10) console.log(num) // 16 // 手写reduce Array.prototype._reduce = function(callback, initialValue){ if(!Array.isArray(this) || !this.length || typeof callback !== 'function'){ return [] } else{ let result = 0 if(typeof initialValue !== 'undefined') { result = initialValue } let len = this.length for(let i=0; i<len; i++){ result = callback(result, this[i]) } return result } } var res = arr._reduce((pre, cur) => { return pre + cur }, 10) console.log(res) // 16
BFC
何为BFC
- BFC(Block Formatting Context)格式化上下文,是Web页面中盒模型布局的CSS渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。
如:一个盒子不设置height,当内容子元素都浮动时,无法撑起自身,那么这个盒子没有形成BFC
如何形成BFC
- 浮动元素,float 除 none 以外的值;
- 定位元素,position(absolute,fixed);
- display 为以下其中之一的值 inline-block,table-cell,table-caption;
- overflow 除了 visible 以外的值(hidden,auto,scroll);
BFC的其他作用
- BFC可以取消盒子的margin塌陷
- BFC可以阻止元素被浮动元素覆盖
数组的扁平化处理
[1, [2, [3, 4], 5], 6] ==> [1, 2, 3, 4, 5, 6]
- 数组自带的方法
- 正则表达式
- 递归
- reduce
const arr = [1, [2, [3, 4], 5], 6] // 1、数组自带的方法 console.log(arr.flat(Infinity)) // [1, 2, 3, 4, 5, 6]
// 2、正则表达式 const tmp = JSON.stringify(arr).replace(/\[|\]/g, '') const res = JSON.parse('[' + tmp + ']') console.log(res) // [1, 2, 3, 4, 5, 6]
// 3、递归 function flat(arr){ let array = [] for(let i=0; i<arr.length; i++){ if(Array.isArray(arr[i])){ array = array.concat(flat(arr[i])) } else{ array.push(arr[i]) } } return array } console.log(flat(arr)) // [1, 2, 3, 4, 5, 6]
// 4、reduce const reduceArr = (arr) => { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? reduceArr(cur) : cur) }, []) } console.log(reduceArr(arr)) // [1, 2, 3, 4, 5, 6]
call、apply、bind
call、apply、bind都是改变this指向的方法
call
-
fun.call(thisArg, arg1, arg2, ...)
-
thisArg
: 在fun函数运行时指定的this值。- 非严格模式下,指定为
null
和undefined
的this值会自动指向全局对象(浏览器中就是window
对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。 - 严格模式下,第一个参数是谁,this就指向谁。
- 非严格模式下,指定为
-
arg1, arg2, ...
指定的参数列表
apply
-
fun.apply(thisArg, [argsArray])
-
apply:和call基本上一致,唯一区别在于传参方式
-
apply把需要传递给fun的参数放到一个数组(或者类数组)中传递进去
bind
- bind:语法和call一模一样,区别在于立即执行还是等待执行
- bind返回的是一个函数,需要调用才执行
代码演示
let tmp = { name: '李四', age: 22 } let obj = { name: '张三', age: 18, show(){ console.log(this.name, this.age) } } // 直接调用时this则指向对象本身 obj.show() // 张三 18 // call改变this的指向 obj.show.call(tmp) // 李四 22 obj.show.apply(tmp) // 李四 22 // bind 返回的是一个函数 let fn = obj.show.bind(tmp) fn() // 李四 22
类的创建与继承
类的创建
创建一个动物类
function Animal(name){ // 属性 this.name = [name] // 实例方法 this.sleep = function(){ console.log(this.name[0] + '正在睡觉') } } // 原型链方法 Animal.prototype.eat = function(){ console.log(this.name[0] + '正在吃东西') }
继承
- 原型链继承
特点:基于原型链,既是父类的实例,也是子类的实例
缺点:无法实现多继承
function Cat(){ } Cat.prototype = new Animal() Cat.prototype.constructor = Cat const cat = new Cat() const cat2 = new Cat() cat.name[0] = '猫' cat.sleep() cat.eat() cat2.name[0] = '小猫咪' cat.sleep() cat.eat()// 猫正在睡觉// 猫正在吃东西// 小猫咪正在睡觉// 小猫咪正在吃东西
- 构造继承
特点:可以实现多继承
缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。
function Dog(){ Animal.call(this) } const dog = new Dog() const dog2 = new Dog() dog.name[0] = '狗' dog2.name[0] = '小狗狗' dog.sleep() dog2.sleep() dog.eat() dog2.eat()// 狗正在睡觉// 小狗狗正在睡觉// Uncaught TypeError: dog.eat is not a function// Uncaught TypeError: dog.eat is not a function
- 组合继承
特点:可以继承实例属性/方法,也可以继承原型属性/方法
缺点:调用了两次父类构造函数,生成了两份实例
function Pig(){ Animal.call(this) } Pig.prototype = new Animal() Pig.prototype.constructor = Pig const pig = new Pig() const pig2 = new Pig() pig.name[0] = '猪' pig2.name[0] = '小猪猪' pig.sleep() pig2.sleep() pig.eat() pig2.eat()// 猪正在睡觉// 小猪猪正在睡觉// 猪正在吃东西// 小猪猪正在吃东西
- 寄生继承
较为推荐
function Bird(){ Animal.call(this) } Bird.prototype = Animal.prototype Bird.prototype.constructor = Bird const bird = new Bird() const bird2 = new Bird() bird.name[0] = '鸟' bird2.name[0] = '小鸟鸟' bird.sleep() bird2.sleep() bird.eat() bird2.eat()// 鸟正在睡觉// 小鸟鸟正在睡觉// 鸟正在吃东西// 小鸟鸟正在吃东西
Promise
概述
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。
主要是为了解决回调地狱的问题。
Promise对象有两个特点:
- 对象的状态不受外界影响(三种状态)
- Pending状态(等待状态)
- Fulfilled状态(满足状态)
- Rejected状态(拒绝状态)
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果(两种状态改变)
- Pending -> Fulfilled
- Pending -> Rejected
Promise三种状态
- pending:等待状态
比如正在进行网络请求,或者定时器没有到事件 - fulfilled:满足状态
当我们主动回调了resolve时,就处于该状态,并且会回调then() - rehected:拒绝状态
当我们主动回调了reject时,就处于该状态,并且会回调catch()
用法
new Promise((resolve, reject) => { // ... some code if (/* 异步操作成功 */) { resolve(value); } else { reject(error); }}).then(value => { // 异步操作成功时执行}).catch(error => { // 异步操作失败时执行})
MVVM开发模式
- MVVM分为Model、View、ViewModel三者
- Model:代表数据模型,数据和业务逻辑都在Model层中定义;
- View:代表UI视图,负责数据的展示;
- ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了Model和View的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作dom。
vue计算属性
计算属性
- 计算属性是用来声明式的描述一个值依赖了其它的值
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:
<div id="example"> {{ message.split('').reverse().join('') }}</div>
在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message
的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。
<div id="example"> <p>Original message: "{{ message }}"</p> <p>Computed reversed message: "{{ reversedMessage }}"</p></div>
var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 return this.message.split('').reverse().join('') } }})
结果:
Original message: “Hello”
Computed reversed message: “olleH”
计算属性缓存 vs 方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中methods: { reversedMessage: function () { return this.message.split('').reverse().join('') }}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。
然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message
还没有发生改变,多次访问reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
计算属性和监听器的比较
- vue的computed主要用于同步对数据的处理,而watch主要用于事件的派发,可异步。
- 两者都能达到相同的效果,基于各自的特点,使用场景会有区分。
- computed拥有**
缓存属性
,只有当依赖的数据发生变化时,关联的数据才会变化,适用于计算或者格式化数据
**的场景 - watch监听数据**
有关联但是没有依赖
,只要某个数据发生变化,就可以处理一些数据或者派发事件并同步/异步执行
**。
vue组件data
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
v-if和v-show的区别
- v-show 仅仅控制元素的显示方式,将 display 属性在 block 和 none 来回切换;
- 而v-if会控制这个 DOM 节点的存在与否。
- 当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。