高频面试题讲解
js数据类型
- 基本类型: number、string、boolean、null、undefined、symbol
- 引用(复合类型)类型: object(函数、对象、数组等)
不同类型的存储方式:
- 基本类型:基本类型值在内存中占据固定大小,保存在栈内存中
- 引用类型:引用类型的值是对象,保存在堆内存中,而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址
symbol类型介绍
Symbol
对象是es6
中新引进的一种数据类型,它的作用非常简单,就是用于防止属性名冲突而产生。还可以实现属性的私有化(private)。
Symbol
的最大特点就是值是具有唯一性,这代表使用Symbol
类型的值能做独一无二的一些事情。
此外,Symbol
没有构造函数,这使得我们不能new
它,直接使用即可。
symbol类型应用场景:
- 设置对象唯一键(防止对象合并属性名冲突)
// 一个班级有两个同学,都叫张三,通过
// 存储和获取的时候通过Symbol()来操作,就唯一了
let user1 = {
name: '张三',
key: Symbol()
};
let user2 = {
name: '张三',
key: Symbol()
};
let score = {
[user1.key]: {html: 90, css: 100},
[user2.key]: {html: 80, css: 90}
}
console.log(score[user2.key])
- 作为对象私有属性,不让别人看到,来模拟类中private属性的效果
Symbols 在 for...in
迭代中不可枚举。另外,Object.getOwnPropertyNames()
不会返回 symbol 对象的属性,但是你能使用 Object.getOwnPropertySymbols()
得到它们。
var obj = {};
obj[Symbol("a")] = "a";
obj["c"] = "c";
obj.d = "d";
for (var i in obj) {
console.log(i); // "c" and "d"
}
注:
Symbol
声明和访问使用[]
(变量)形式操作,不能使用.
语法因为.
语法是操作字符串属性的
当使用 JSON.stringify() 时,以 symbol 值作为键的属性会被完全忽略:
js基本包装类型
js除了基本数据类型和引用数据类型外,JavaScript还提供了三种基本包装对象:number
、boolean
和string
。
let str = 'abc'
typeof str; // 'string'
let str2 = new String('abc')
typeof str; // 'object'
str === str2; // false
虽然包装对象看上去和原来的值一模一样,用typeof查看他们的类型已经变为object
了!所以,包装对象和原始值用===
比较会返回false
:
基本(原始)类型和基本包装类型的区别就是生存期不同,在代码执行后就会销毁实例。
var str = "abc";
str.age= "18";
console.log(str.age) ;// undifined第二行中给str添加了age属性,在代码执行后就会销毁,第三行再次访问的时候,age就不见了; 但通过构造函数形式是不会销毁的。
var str = new String('abc');
str.age= "18";
console.log(str.age); // '18'
但同时包装类型也具备 与各自基本类型相应的特殊行为。
实际上:每当读取一个基本类型值的时候, “后台就会创建一个 对应的基本包装类型的对象”,从而能够调用其相应原型上面的一些属性和方法来操作这些数据。
let str = 'abc';
str.length; // 3
str.toUpperCase(); // ABC
判断一个变量的数据类型
Object.prototype.toString.call
用来判断类型再合适不过,借用它我们几乎可以判断所有类型的数据:
而之前的typeof
如果用来判断数据类型,数组对象的结果都是object
类型,就无法准确区变量是数组还是对象。
// 借用Object.prototype.toString.call()获取数据类型
Object.prototype.toString.call(变量);
如判断一个变量是否是数组?
function isArray(arr){
if(Object.prototype.toString.call(arr) == '[object Array]'){
return true;
}
return false;
}
console.log( isArray(['abc']) ); // true
console.log( isArray({name:"大白"}) ); // false
或者借助es6的Array.isArray来判断
获取精确的数据类型
function typeOf(obj) {
const toString = Object.prototype.toString; // 获取原始的toString方法
const map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]': 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
return map[toString.call(obj)];
}
this指向问题
记住一句话:this永远指向最后的调用者。
专业点说:this指向其绑定所在函数执行上下文中的this.
如果在全局中调用,函数中this一般是window,在严格模式下window为undefined。
也可以通过bind/call/apply显式改变this的指向。
在 箭头函数中,this绑定其父级上下文中的this.
var obj = {
name: "24"
getName: function(){
console.log(this)
console.log('名字' + this.name)
}
}
obj.getName(); // this 指向obj
var func = obj.getName;
func(); // this 指向 window 实质上是全局window对象在调用 ,
DOM中的this:
<body>
<!-- this: 代表当前绑定的标签DOM对象 -->
<button onclick="foo(this)">btn</button>
</body>
<script>
function foo(ele){
console.log(ele.innerHTML);
}
</script>
一些常见的DOM操作不要忘记。
- jquery中的this
$("#box).click(function(){
console.log(this); // 代表当前绑定的标签DOM对象
// 转出jquery对象
console.log( $(this) )
})
- .vue单文件中的this
this代表当前组件的components对
改变this指向
通过 bind/call/apply
语法:
函数名.bind(对象,参数1,....)
函数名.call(对象,参数1,....)
函数名.apply(对象,[参数1,....]])
三种相同点: 都可以改变函数内this的指向
不同点:
- call/apply是立即执行函数
- bind返回的是一个函数,并已经改变其中的this. 需要自己触发执行。
传参方式:
- call参数一个一个传递,bind也是一样
- apply传递参数数组
作用域和作用域链
-
作用域
定义了标识符(变量名、函数名)的有限访问范围。
仅有函数才会产生局部(函数)作用域
-
什么是作用域链:
函数内部访问函数外部的标识符(变量名,函数名),首先会在当前作用域中查找,若找不到,则去父级作用域中找,直到找到全局,这种按链式结构查形成的结构,就是作用域链。
原型和原型链
- 什么是原型:
每个函数或对象都有__proto__属性 它的值是一个对象 即原型对象
- 什么是原型链
通过构造函数创建的某个对象,当对象使用属性的时候,先在自身内存空间中进行查找,有就直接使用,若没找到就会沿__proto__属性找到其对象的原型,直到找到Object.prototype原型对象,最终属性找不到则返回undefined,方法找不到则报错。这种寻找过程形成的链式结构就是原型链
js继承。
-
for-in继承
-
call/apply伪造(构造函数)继承: 去父类构造函数中伪造this,把构造函数属性添加子类对象的自身空间中。
-
原型继承: 继承父类原型中的属性或方法,核心还是父类的对象
__proto__
找到父类的原型
子类.prototype = new 父类();
- 组合继承(call伪造+原型继承)
- es6 class-extends实现继承
闭包-Closure(重点)
- 什么是闭包(谈下你对闭包的理解)
- 怎样产生闭包?
- 闭包优缺点
- 你在哪些场景使用过闭包,即用闭包解决过哪些实际问题
-
单例模式
-
事件性能优化-防抖节流 参考
函数防抖(debounce)和函数节流(throttle)都是为了缓解函数频繁调用,它们相似,但有区别. 函数节流:在设定的时间间隔内只会执行一次任务。如果在同一个单位时间内某事件被触发多次,只有一次能生效 函数防抖:只有任务触发的时间间隔超过设定的间隔后,任务才会执行一次。如果在这时间间隔内又被触发,则重新计时。 - 节流主要用在鼠标事件上 - 防抖主要用在键盘事件上
-
模块化编程(jquery库)
-
函数科里化
-
解决for循环引用问题, 及for中使用延时器setTimeout。 用let或IIFE解决。
-
…
-
函数科里化(理解)
概念:
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数
实现 add(1)(2, 3)(4)() = 10
的效果
依题意,有两个关键点要注意:
- 传入参数时,代码不执行输出结果,而是先把所有参数先用闭包记忆起来
- 当传入空的参数时,代表可以进行真正的运算
完整代码:
function currying(fn){
var allArgs = []; //闭包变量 用来接受所有的参数
return function next(){
// 将伪数组转为真数组,
// var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);
if(args.length > 0){
allArgs = allArgs.concat(args); // 传参了,返回闭包函数
return next;
}else{
return fn.apply(null, allArgs); // 无参数,执行传入的函数
}
}
}
var add = currying(function(){
var sum = 0;
for(var i = 0; i < arguments.length; i++){
sum += arguments[i];
}
return sum;
});
柯里化,在这个例子中可以看出很明显的行为规范:
- 逐步接收参数,并缓存供后期计算使用
- 不立即计算,延后执行
- 符合计算的条件,将缓存的参数,统一传递给执行方法
相关文档:
对深浅拷贝(克隆)的理解,如何实现?
浅拷贝:浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
相关参考
通过JSON.stringify
也行,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。但是一些特殊值序列化不了,如undefined、function(){}、symbol
。
一般可以借助第三方库实现,如 lodash函数工具库的以下两个方法
_.clone(value) : 创建一个 value 的浅拷贝(只拷贝第一层)。或 Object.assign()
_.cloneDeep(value): 递归拷贝 value。(注:也叫深拷贝)。
总而言之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
手动实现深拷贝: 通过递归实现
// 实现深拷贝:
// 主函数
function deepCopy(data){
// 只有引用类型才实现深拷贝 []、{}
if(typeof data === 'object'){
if( Array.isArray(data) ){
// 数组
return copyArray(data);
}else {
// 对象
return copyObject(data)
}
}
// 基本类型默认是深拷贝
return data;
}
// 深拷贝数组
function copyArray(arr){
// arr => [a,{},1,[]]
var newArray = []; // 相当于是一个新的空间
arr.forEach(item => {
if(typeof item === 'object'){
// 递归调用自己
newArray.push(deepCopy(item))
}else{
newArray.push(item)
}
})
return newArray;
}
// 深拷贝对象
function copyObject(obj){
// obj => {a:1,b:[]}
var newObj = {}; // 相当于是一个新的内存空间
for(let key in obj) {
if(typeof obj[key] === 'object'){ // obj {name:'age'} obj[name] => age
newObj[key] = deepCopy(obj[key])
}else{
newObj[key] = obj[key]
}
}
return newObj;
}
或:
function deepCopy(data) {
const t = typeOf(data);
let o;
if (t === 'array') {
o = [];
} else if ( t === 'object') {
o = {};
} else {
return data;
}
if (t === 'array') {
for (let i = 0; i < data.length; i++) {
o.push(deepCopy(data[i]));
}
} else if ( t === 'object') {
for (let i in data) {
o[i] = deepCopy(data[i]);
}
}
return o;
}
ES6常用特性
- let、const、箭头函数、对象简写、解构赋值、展开运算符、promise、asyng/await、数组方法map/filter等
数组中的reduce方法
- reduce方法接受一个函数作为累加器(“连续操作器”)
- reduce为数组中的每一个元素依次执行回调函数。最终返回最后一次调用累加器的结果
- 语法:
arr.reduce(callback(accumulator, currentValue,[index],[array]), initialValue)
callback四个参数:
accumulator 累计器
currentValue 当前值
currentIndex 可选,当前索引
array 可选,数组
initialValue
可选,累加器函数默认值
**注意:**如果没有提供
initialValue
,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue
,从索引0开始
代码示例:
var arr = [1,2,3,4];
var result = arr.reduce(function (accumulator, currentValue, index) {
// console.log(accumulator, currentValue, index)
return accumulator + currentValue;
});
console.log(result) // 10
var result = arr.reduce(function (accumulator, currentValue, index) {
// console.log(accumulator, currentValue, index)
return accumulator + currentValue;
},10);
console.log(result) // 10
reduce在mvvm中的应用,通过key获取value
理解JavaScript执行上下文和执行栈
什么是执行上下文:
简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文类型:
执行上下文总共有三种类型:
-
全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
-
函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列操作。
执行上下文的生命周期包括三个阶段:创建阶段→执行阶段→回收阶段
- 创建阶段:1. 创建变量对象:首先初始化函数的参数arguments ,提升函数声明和变量声明 2.创建作用域链(Scope Chain) 3. 确定函数内this指向
- 执行阶段:执行变量赋值、代码执行
- 回收阶段: 执行上下文出栈等待虚拟机回收执行上下文
-
Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,eval函数会将传入的字符串当做 JavaScript 代码进行执行行,非常危险。所以在这里不再讨论。
console.log(eval(‘2 + 2’)); // 4
eval(‘alert(“攻击”)’)
执行上下文栈
函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?
JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZXtcZpX-1623755833649)(http://note.youdao.com/yws/res/98/WEBRESOURCE98c81870b081d16959f62f86e62b3d01)]
从上面的流程图,我们需要记住几个关键点:
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
我们再来看个例子:
var color = 'blue';
function changeColor() {
var anotherColor = 'red';
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
上述代码运行按照如下步骤:
- 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
- 调用 changeColor函数时,此时changeColor函数内部代码还未执行,js执行引擎立即创建一个changeColor的执行上下文(简称EC),然后把这执行上下文压入到执行栈(简称ECS(stack))中。
- 执行changeColor函数过程中,调用swapColors函数,同样地,swapColors函数执行之前也创建了一个swapColors的执行上下文,并压入到执行栈中。
- swapColors函数执行完成,swapColors函数的执行上下文出栈,并且被销毁。
- changeColor函数执行完成,changeColor函数的执行上下文出栈,并且被销毁。
参考:
DOM和BOM区别
- DOM:
doucment object model
,文档对象模型,提供了给一套操作文档元素的api。如操作属性、类型、文本等、事件等。- className/classList: 类名操作
- setAttribute/getAttribute/removeAttribute: 属性操作
- style.fontSize: 样式操作
- innerHTML/innerText/value: 文本操作
- 一些事件如单击onclick、键盘onkeyup、鼠标onmousedown、onmousemove、onmouseup等。
- BOM:
Browser object model
,浏览器对象模型,提供了给一套操作浏览器的api。如前进、后退、刷新、跳转、获取屏幕分辨率,浏览器信息(navigator.userAgent,判断pc还是移动端)等
jQuery使用
jquery是一个操作页面dom元素的插件(库),常用的操作如:样式、属性、类名、文本、动画、绑定事件、发送ajax等。
-
$(‘css选择器’): 选择器
-
dom和jquery对象转换
转jquery对象: $(DOM对象) 转DOM对象: jquery对象[下标], 或者 jquery对象.get(下标)
-
css(): 样式操作
-
attr/prop: 属性操作
-
addClass/removeClass: 类名操作
-
html()/val(): 文本操作
-
on/bind/事件名:绑定事件。 on还可以实现事件委托
$(父元素).on('click','子元素',callback)
-
事件流阶段:捕获阶段-》目标阶段-》冒泡阶段
-
阻止事件默认行为和冒泡。
-
事件委托的原理:冒泡。
-
冒泡的原理:根据鼠标点击的xy坐标如果在父元素范围内,则会被父元素感知到,则触发父的事件
-
哪些事件会冒泡:有onclick、onmouseover/onmouseout
onmouseenter/onmouseleave不会冒泡
-
unbind/off:事件解绑
-
$.get/$.post/$.ajax/$.getJson
: ajax请求 -
$.ajax上传文件 + formData ,或者 原生ajax + formData
核心:post请求传递二进制和文本数据,设置请求头content-type为multipart/form-data
$.ajax({ type:'post', data: new FormData(表单dom对象), processData: false, # 代表不处理数据 contentType: false, #设置内容类型 multipart/form-data success:function(){ } })
-
$.serialize
: 序列化表单元素
什么是跨域,为什么会有跨域限制,如何解决跨域?
什么是跨域,为什么会有跨域限制
- 所谓的跨域就是浏览器为了安全起见,而采用的同源策略(Same origin policy),阻止获取不同源中的数据信息。
- 同源略是 Netscape (网景公司)提出的一个著名的安全略,所有支持 Javascript 的浏览器都会使用这个策略,
- web 是构建在同源策略之上的,浏览器只是针对同源策略的一种实现
- 所谓同源是指:协议、域名(IP),端口号 要一致才行。这样才算是在同一个域里
- 如何才算跨域,请求与被请求的
协议、域名、端口
有不一样则就是跨域
如何解决跨域
开发环境: cors、webpack(devserver proxy代理)
生产环境:cors、nginx服务器代理
原生事件实现拖拽
原生实现需要三个事件: onmousedown、onmousemove、onmouseup
非同源受到哪些限制?
- Cookie不能读取
- DOM无法获取
- Ajax请求不能发送
没有同源策略的危险场景
危险场景:有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进 www.yinghang.com 改密码,点击去之后网页会有类似下面代码:
<iframe id='baidu' src='https://www.baidu.com'></iframe>
<script>
let iframe = window.frames['baidu']
let value = iframe.document.getElementById('含有敏感信息的input框的id,如密码,银行账号')
</script>
iframe标签 非常危险,基本不用
如何解决跨域问题(三种)
-
代理
-
nginx代理 ( nginx proxy_pass配置反向代理)
-
cors。服务端设置响应头允许跨域。需要服务端配合
-
jsonp。 jsonp不是一种技术,它是程序员
智慧的结晶
- 原理:利用script标签的src属性不受同源策略限制的特点,服务端响应一个符合js函数调用语法的字符串。前端执行函数,从而达到跨域。
- 局限性: 只能发送get请求,必须后端配合
开发环境:代理proxy、cors
生产(线上)环境:nginx代理、cors
前端代理: vue.config.js
module.exports = {
// 只适用于开发环境
devServer: {
proxy: {
'/api': {
target: 'http://api.w0824.com/',
ws: true,
// changeOrigin 设置为true后,target才可以是一个域名
changeOrigin: true
},
'/wx': {
target: 'http://api.wx.com/',
ws: true,
// changeOrigin 设置为true后,target才可以是一个域名
changeOrigin: true
}
}
}
}
请求:
import axios from 'axios';
export default {
name: 'App',
methods:{
async getLunbo(){
// var apiulr = "http://api.w0824.com"
var res = await axios.get('/api/getlunbo');
console.log(res)
}
}
}
异步发展历程
-
var xhr = new XMLHttpRequest ActiveXObject(‘Microsoft.XMLHTTP’)
get-4步骤, post-五部
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if(this.readyState == 4 && this.status == 200){ // data '{"name":"kobe"}' '[{"name":"kobe"},{"name":"jame"}]' var data = this.resonseText(); // data json字符串 var result = JSON.parse(data) // 把数据做一些业务逻辑 } } xhr.open('post','url',true) // 模拟post表单传递数据 xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded') xhr.send();
-
jquery-ajax $.get $.post
$.ajax()实现文件上传
原生post请求传递二进制和文本数据,核心post,设置请起头content-type为multipart/form-data
$.ajax({ type:'post', data: new FormData(表单dom对象), processData: false, # 代表不处理数据 contentType: false, #设置内容类型 multipart/form-data success:function(){ } })
get也可以实现文件上传,但要把图片文件变成base64编码即可 img src=“base64”
base64一般只需要把小图变成base64即可 webpack js .css-loader file-loader?limit=2048
-
Promise
- promise解决了什么问题 ? 解决了回调地狱问题
- 什么是回调地狱:回调函数会层层嵌套,导致可读性差
- 如何把原生ajax封装成一个promise版本
-
window.fetch (仅浏览器支持,返回一个promise)
-
async/await。异步终极解决方案。 (async/await本质是生成器
* yield
的语法糖形式)var promiseArr = []; console.time('query time') arr.map(async (v)=>{ promiseArr.push( query(v.sql) ) }) // 并行执行多个异步请求,效率更高 var result = Promise.all(promiseArr) console.endTime('query time')
生成器generator
Generator函数的强大在于**允许你通过一些实现细节来将异步过程隐藏起来,**依然使代码保持一个单线程、同步语法的代码风格。这样的语法使得我们能够很自然的方式表达我们程序的步骤/语句流程,而不需要同时去操作一些异步的语法格式。
示例1:
function* myGenerator(){
yield 1; // yield 提前返回
yield 2;
yield 3;
}
var gen = myGenerator();
// 通过next获取生成器下一次返回的值
// console.log(gen.next())
// console.log(gen.next())
// console.log(gen.next())
// console.log(gen.next())
// console.log(gen.next())
// var result;
// while(!(result = gen.next()).done){
// console.log(result.value)
// }
// for(var i of gen){
// console.log(i)
// }
示例2: 生成器和异步的结合
Generator函数的强大在于允许你通过一些实现细节来将异步过程隐藏起来,依然使代码保持一个单线程、同步语法的代码风格。这样的语法使得我们能够很自然的方式表达我们程序的步骤/语句流程,而不需要同时去操作一些异步的语法格式。
也就是说,Generator 函数相当于就是一个异步操作的容器
<script>
function* myGenerator(url) {
yield fetch(url);
}
var gen = myGenerator('http://api.w0824.com/api/getlunbo');
/*
// 或者写成函数自执行形式
var gen = (function * (url){
yield fetch(url);
})('http://api.w0824.com/api/getlunbo')
*/
var promise = gen.next().value
promise
.then(res => res.json())
.then(data => {
console.log(data)
})
</script>
nodejs中可以借助axios发送异步请求:
var axios = require('axios');
console.log('1')
var gen = (function * (url){
yield axios.get(url);
})('http://api.w0824.com/api/getlunbo')
var promise = gen.next().value
promise.then(res => {
console.log(res.data)
})
console.log('3')
Co函数库
co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行。
co 函数库可以让你不用编写 Generator 函数的执行器(即不用写next)
var co = require('co');
co(gen);
上面代码中,Generator 函数只要传入 co 函数,就会自动执行。
co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
})
基本 使用:
var axios = require('axios');
var co = require('co')
var gen = function * (url){
var result = yield axios.get(url);
return result;
};
co(gen('http://api.w0824.com/api/getlunbo')).then(res => {
console.log(res.data)
})
async和await终极解决方案
async/await本质是 生成器* yield
的语法糖形式,书写起来更加便捷
var axios = require('axios');
var co = require('co')
var gen = async function (url){
var result = await axios.get(url);
return result;
};
co(gen('http://api.w0824.com/api/getlunbo')).then(res => {
console.log(res.data)
})
事件循环 event loop 宏任务 微任务
- js是单线程的,nodejs也是一样,基于chrome的v8引擎实现。
- 同步任务会直接进入主线程依次执行
- 异步任务会再分为宏任务和微任务
执行顺序:主线程中的同步代码执行完毕后,只剩下微任务和宏任务,顺序按照先微后宏来执行。
function test() {
console.log(1)
setTimeout(function () { // timer1
console.log(2)
}, 1000)
}
test();
setTimeout(function () { // timer2
console.log(3)
})
new Promise(function (resolve) {
console.log(4)
setTimeout(function () { // timer3
console.log(5)
}, 100)
resolve()
}).then(function () {
setTimeout(function () { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
// 结果: ?
补充关于 async/await 函数
setTimeout(() => console.log(4)) //(宏任务)
async function test() {
console.log(1); // await前: 相当于 new Promise构造函数中的同步代码
await Promise.resolve()
console.log(3); // await后: 相当于 Promise.then 中的异步代码(微任务)
}
test()
console.log(2);
// 结果 ? 1 2 3 4
-
推荐阅读
- Tasks, microtasks, queues and schedules 任务,微任务,队列和时间表
Node服务端
用过node的哪些模块,如
- fs: 处理文件相关的
- path: 处理路径相关的
- util:提供一些常用的方法,如promisify方法把异步函数变成一个promise版本
- http:可以用来发送请求,搭建简易的web服务器
- url:处理请求url相关的信息
- …
express框架
- 用来做过什么? 1.搭建后台管理系统 2。搭建api 接口服务
- 中间件理解?可以在请求过程中对数据进行一系列的拦截和过滤处理。如对req对象进行修改、验证用户的权限等等。
- 有哪些内置中间件?
- 错误中间件
- 托管静态资源
- 解析post请求体参数
- cookie中间件
- 用过哪些第三方中间件
restful接口风格规范
核心:用正确的http方法对资源(数据)进行相应的操作
- get请求:获取资源
- post请求:新增资源
- put请求:更新资源
- delete请求: 删除资源
Linux
常用命令:
- ls、pwd、cd、vim、touch、echo、rm、rmdir、mkdir、ps、pkill、killall、netstat
nginx
-
部署一些静态资源
-
反向代理。如代理node服务如:3000端口
mysql
-
增删改查的sql:
- 查询select
- 删除delete
- 更新update
- 增加insert into
- 联表 left join
-
联表join,各联表的区别
内链接:查询两个边的共同满足的数据。即两个表的交集
左联接:左边为主表,查询主表的所有数据,右表未匹配到的用null来代替
右联接:右边为主表,查询主表的所有数据,左表未匹配到的用null来代替
-
一些常用的优化手段
前后端分离,JWT授权的原理
- 过程。基本思路如下
- 用户输入账号登录请求后台服务器,
- 服务器验证用户的信息,并生成用户信息token返回给前端。
- 前端可以在cookie或localstore进行存储token。
- 后续的每次请求可以通过自定义请求头把token带上,服务端验证token,从而获取用户的信息
- token安全性; 1. 增加有效期(时效性) 2. 不存储密码字段,如密码等
从地址栏输入一个url,到看到页面,中间经历过了哪些过程?
大致流程
- URL 解析
- DNS(Domain Name System) 查询
- TCP 连接
- 处理请求
- 接受响应
- 渲染页面
什么是协议缓存和强缓存
对于IE来说:
只要请求的地址不发生变化,那么直接走强缓存。不会发送网络请求。状态码为 200
对于chrome和firefox
只要请求的地址不发生变化,尝试请求协商缓存(后端根据请求头标识符来判断)。 会发送网络请求,状态码为304。
相关参考:
http协议和https协议
相关参考:
http状态码:
- 1xx: 指示信息,表示请求已经接收,继续处理。
- 2xx: 成功,表示请求已经被成功接收。
- 3xx: 重定向,要完成请求必须进行更近一步操作
- 4xx: 客户端错误,请求有语法错误或请求无法实现
- 5xx: 服务端错误,服务器未能实现合法的请求
常见状态码:
- 200——表明该请求被成功地完成,所请求的资源发送回客户端
- 304——自从上次请求后,请求的网页未修改过,请客户端使用本地缓存(命中了协商缓存)
- 400——客户端请求有错
- 401——请求未经授权
- 403——禁止访问
- 404——资源未找到
- 500——服务器内部错误
- 503——服务不可用
常用的请求头部(部分):
-
Accept: 接收类型,表示浏览器支持的MIME类型(对标服务端返回的Content-Type)
-
Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
-
Content-Type:客户端发送出去实体内容的类型
-
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
-
If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
-
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
-
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
-
If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
-
Cookie:有cookie并且同域访问时会自动带上
-
Connection:当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
-
Host:请求的服务器URL
-
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
-
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
-
User-Agent:用户客户端的一些必要信息,如UA头部等
常用的响应头部(部分):
- Access-Control-Allow-Headers: 服务器端允许的请求Headers
- Access-Control-Allow-Methods: 服务器端允许的请求方法
- Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
- Content-Type:服务端返回的实体内容的类型
- Date:数据从服务器发送的时间
- Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
- Last-Modified:请求资源的最后修改时间
- Expires:应该在什么时候认为文档已经过期,从而不再缓存它
- Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
- ETag:请求变量的实体标签的当前值
- Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
- Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
- Server:服务器的一些相关信息
一般来说,请求头部和响应头部是匹配分析的, 如:
请求头部的
Accept
要和响应头部的Content-Type
匹配,否则会报错。跨域请求时,请求头部的
Origin
要匹配响应头部的Access-Control-Allow-Origin
,否则会报跨域错误在使用缓存时,请求头部的
If-Modified-Since
、If-None-Match
分别和响应头部的Last-Modified
、ETag
对应
TCP协议连接三次握手和断开四次挥手
1. tcp连接三次握手的意义:
获取到服务器的IP, 三次握手才能确认双方的接收与发送能力是否正常。
简单理解:
-
第一次握手:由浏览器发给服务器,我想和你说话,你能 “听见” 吗?
-
第二次握手:由服务器发给浏览器,我能听见,你说吧!
-
第三次握手:由浏览器发给服务器,好,那我开始说话了。
2. tcp断开连接四次挥手的意义:
确保数据的完整性。
简单理解:
- 第一次挥手:由浏览器发给服务器,我的东西接受完了,你关闭吧。
- 第二次挥手:由服务器发给浏览器,我还有一些东西没接收完,你等一会,我接收好了我告诉你。
- 第三次挥手:由服务器发给浏览器,我接收完了,你断开吧!
- 第四次挥手:由浏览器发给服务器,好的,那我断开了。
相关参考:面试官,不要再问我三次握手和四次挥手
web性能优化有哪些手段,具体怎么实现
- 精灵图(雪碧图)
- 防抖节流
- CDN加速
- css、js合并压缩
- 避免重排和重绘
- 利用缓存
- …太多
移动端常见问题
移动端点击事件延迟
移动端浏览器在触发点击事件的时候,通常会出现300ms左右延迟的问题
原因: 移动端的双击会缩放导致click判断延迟
解决办法:
- 禁止缩放 user-scalable=no
<meta name="viewport" content="user-scalable=no" >
-
利用
fastclick.js
插件。 参见fastclick// jQ中使用 $(function() { FastClick.attach(document.body); }); // 原生中使用 if ('addEventListener' in document) { document.addEventListener('DOMContentLoaded', function() { FastClick.attach(document.body); }, false); } // vue中使用 npm i fastclick var attachFastClick = require('fastclick'); attachFastClick(document.body);
移动端touch事件穿透(点透)问题
例如:一个a链接如果上层有个遮盖层,点击遮盖的touch事件会触发a链接默认行为,这就是事件穿透。
我们希望只触发遮盖的touch事件。
解决办法:
- 点击遮盖阻止默认行为
e.preventDefault();
- 借助
fastclick.js
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
简述mvvm的原理
- vue2 使用 Object.defineproperty进行数据劫持 和 利用观察者模式,观察到数据变更在通知页面渲染。
- vue3 采用代理 es6-proxy ,代理对象
相关参考:
vue面试题
Vue的双向数据绑定原理是什么?
vue.js 是采用数据劫持结合发布者-订阅者模式(观察者模式)的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤
1、需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
2、compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
4、MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
观察者模式,它是一对多的一种模式。
在Vue中,一 代表什么?代表data中的某个数据
多代表什么?就是页面上凡是使用了这个数据的地方,都要更新。
就是页面上的很多”地方“,都观察着这个data,这就是一对多关系,所以用观察者模式来实现。
手写mvvm源码,实现双向绑定,实现基本的指令 v-html v-text v-model 等,
vue如何监听对象或者数组某个属性的变化
当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。因为在Vue中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
解决方式:
this.set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
this.set(this.arr, 0, "OBKoro1"); // 改变数组
this.$set(this.obj, "c", "OBKoro1"); // 改变对象
或者这样解决,经过vue内部处理后可以使用以下几种方法来监听数组
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue里,是通过递归以及遍历data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。
而在Vue3中取代它的Proxy有以下两个优点;
- 可以劫持整个对象,并返回一个新对象
- 有13种劫持操作
vue组件的通信方式有哪些
- 父子组件: 自定义事件机制 和 props
- 兄弟组件: eventBus、vuex
- 所有组件: vuex共享组件数据 ,且vuex中数据是响应式的
为什么vue组件中data必须是一个函数?
对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题
vue中v-if和v-show有什么区别?
v-if和v-show看起来似乎差不多,当条件不成立时,其所对应的标签元素都不可见,但是这两个选项是有区别的:
1、v-if在条件切换时,会对标签进行适当的创建和销毁,而v-show则仅在初始化时加载一次,因此v-if的开销相对来说会比v-show大。
2、v-if是惰性的,只有当条件为真时才会真正渲染标签;如果初始条件不为真,则v-if不会去渲染标签。v-show则无论初始条件是否成立,都会渲染标签,它仅仅做的只是简单的CSS切换
computed和watch的区别
compute计算属性:
支持缓存,只有依赖数据发生改变,才会重新进行计算
不支持异步,当computed内有异步操作时无效,无法监听数据的变化
computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值
如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed
如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。
侦听属性watch:
侦听属性watch:
不支持缓存,数据变更,直接会触发相应的操作;watch支持异步;监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;当一个属性发生变化时,需要执行对应的操作;一对多;监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数
$nextTick是什么?
nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用nextTick,则可以在回调中获取更新后的 DOM
v-for 中key的作用?
当Vue用 v-for 正在更新已渲染过的元素列表是,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue将不是移动DOM元素来匹配数据项的改变,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
为了给Vue一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。key属性的类型只能为 string或者number类型。
key 的特殊属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。如果不使用 key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key,它会基于key的变化重新排列元素顺序,并且会移除 key 不存在的元素
vue如何获取dom?
先给标签设置一个ref值,再通过this.$refs.domName获取,例如:
<div ref="test"></div>
const dom = this.$refs.test
slot插槽
封装组件时候使用的,组件中的内容有时候不能写死,应该由用户决定。可以根据插槽名称控制组件的内容展示。
vue初始化页面闪动问题
使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。
Vuex
Vuex 实现组件中的数据共享。且数据是响应式的。
几个特性:
- state: state 唯一数据源。
- getters: 可以认为是 store 的计算属性,就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。代码中通过 store.getters 触发
- mutation: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,非常类似于事件,通过store.commit 方法触发
- action: Action 类似于 mutation,不同在于Action 提交的是 mutation,而不是直接变更状态,Action 可以包含任意异步操作
- module 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块中也可以有自己的state、getters、mutation、action
ajax请求代码应该写在组件的methods中还是vuex的actions中
如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。
如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用
vuex中的数据在页面刷新后数据消失
解决办法:用sessionstorage 或者 localstorage 存储数据。
可以引入插件vuex-persist来解决,使用方法如下:
npm install --save vuex-persist // 安装
import VuexPersistence from 'vuex-persist' // 引入
const vuexLocal = new VuexPersistence({ // 先创建一个对象并进行配置
storage: window.localStorage
})
const store = new Vuex.Store({ // 引入进vuex插件
state: { ... },
mutations: { ... },
actions: { ... },
plugins: [vuexLocal.plugin]
})
通过以上设置,如果刷新某个视图,数据并不会丢失,依然存在,并且不需要在每个 mutations 中手动存取 storage 。
怎么在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
组件中重复使用mutation
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
然后调用this.setNumber(10)相当调用this.$store.commit(‘SET_NUMBER’,10)
mutation和action有什么区别
action 提交的是 mutation,而不是直接变更状态。mutation可以直接变更状态
action 可以包含任意异步操作。mutation只能是同步操作
提交方式不同:
action 是用this.store.dispatch('ACTION_NAME',data)来提交。
mutation是用this.$store.commit('SET_NUMBER',10)来提交
在v-model上怎么用Vuex中state的值?
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
前端路由:
前端路由是现代SPA(单页应用)应用必备的功能,每个现代前端框架都有对应的实现,例如vue-router、react-router
hash路由一个明显的标志是带有#
,我们主要是通过监听url中的hash变化来进行路由跳转。
多页面应用(MPA)
多页面(MPA),就是指一个应用中有多个页面,页面跳转时是整页刷新。
单页面的优点:
- 用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小。
- 前后端分离。
- 页面效果会比较炫酷(比如切换页面内容时的专场动画)。
单页面缺点:
- 不利于seo。
- 导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理)。
- 初次加载时耗时多。
- 页面复杂度提高很多
由于hash路由的优势就是兼容性更好,在老版IE中都有运行,问题在于url中一直存在#
不够美观,而且hash路由更像是Hack而非标准,相信随着发展更加标准化的History API会逐步蚕食掉hash路由的市场。
router和route的区别
route为当前router跳转对象里面可以获取name、path、query、params等
router为VueRouter实例,想要导航到不同URL,则使用router.push方法
路由按需加载
随着项目功能模块的增加,引入的文件数量剧增。如果不做任何处理,那么首屏加载会相当的缓慢,这个时候,路由按需加载就闪亮登场了。
{
path:'/',
name:'home',
components:()=>import('@/components/home')
}
import()方法是由es6提出的,动态加载返回一个Promise对象,then方法的参数是加载到的模块。类似于Node.js的require方法,主要import()方法是异步加载的。
axios封装网络请求
参考下 Vue 中 Axios 的封装和 API 接口的管理 即可
核心是:
- 请求拦截器中设置token授权等信息,可结合vuex
- 响应拦截器中对返回的不同状态码做统一处理
也可以封装独立的get或post请求
pc端: element-ui 、Ant Design of Vue
常用webpack配置
如:devServer 选项中配置proxy代理跨域
devServer: {
//配置开发服务器
host: "0.0.0.0",
//是否启用热加载,就是每次更新代码,是否需要重新刷新浏览器才能看到新代码效果
hot: true,
//服务启动端口
port: "8080",
//是否自动打开浏览器默认为false
open: false,
//配置http代理
proxy: {
"/api": { //如果ajax请求的地址是http://192.168.0.118:9999/api1那么你就可以在jajx中使用/api/api1路径,其请求路径会解析
// http://192.168.0.118:9999/api1,当然你在浏览器上开到的还是http://localhost:8080/api/api1;
target: "http://192.168.0.118:9999",
//是否允许跨域,这里是在开发环境会起作用,但在生产环境下,还是由后台去处理,所以不必太在意
changeOrigin: true,
pathRewrite: {
//把多余的路径置为''
"api": ""
}
},
"/api2": {//可以配置多个代理,匹配上那个就使用哪种解析方式
target: "http://api2",
// ...
}
}
}
参考:常用webpack配置
简述diff算法
小程序
- 授权登录流程
- 微信支付流程
- 常用的一些api。如获取用户信息、获取手机号码、定位、扫码、系统信息、网络状态、发请求等。
web网络安全
- xss攻击(js攻击)
- sql注入
- csrf伪造已登录用户进行攻击
- …