首先很感谢之前收藏关注的各位,也希望我能给大家带来更多的帮助。这一片推文的基础是基于之前的 “每天一个知识点” 的。在之前每天更新的过程中,我发现了好几个问题,一个是知识点太散乱了(这不能怪我呀,哈哈哈,每天都是想到啥就写啥),这样不能带来体系化,也不便于关联性的知识点互相借鉴。还有的问题就是排版很乱,没有统一的规范,某一段可能加粗来表述重点,某个地方又用红底来表示。还有的问题就是很多知识点深入的不够,太肤浅了。
所以针对这些问题,我以上一篇为基础做出了改变。排版上按所属来划分,并且有关联的点会顺序排布(因为某个东西的出现往往是为了解决上一个问题的缺陷的)。同时我给自己定义了一套规范,全篇的排版都是按照一定的规则来定义的。另外对于一些更深入理解后的知识,对他进行补充深化。
感谢大家,还有什么好的建议也可以告诉我哦!
原则上每个知识点我会按照为什么来进行解释,这也是我和之前的小伙伴一起每日分享的思想,是什么,为什么,怎么做。其实在我刚接触前端的时候,我觉得前端真的又多东西,还杂乱,很多还深,学了要是不用的话,两下子忘了。一直哼苦恼,后面我才体会到一句话 “知其然,知其所以然”,是的,一样东西你不知道他为什么出现,解决什么问题,那你怎么去理解他。这也就成了我的座右铭,也是我学的方针,从此学习就不会迷茫啦~
使用说明🤞🤞🤞
- 知识点会根据类型来分部,也就是各个大专题,比如 JavaScript / CSS / 计算机网络等等
- 其次对于面试常常问到的会更加出现的频率,划分等级重要性
- 必问 :⭐⭐⭐⭐⭐(必须完全理解,作用,场景,同类,且能变通联系相关内容)
- 高频: ⭐⭐⭐(理解透彻,能说出作用,使用场景)
- 低频: ⭐(明白作用,使用场景)
- 其次还会对于一下知识点添加面试回答(哈哈哈,基于我个人的回答,其实就是提供一个回答的基本要点,表达的方式,因为有些时候我发现就算我明白这个东西但是面试的时候就是说不好~~~其实吧,我觉得介绍回答面试官的问题的时候,你要结合自己的经验来说,这样既不会显得蠢蠢的背八股文,其次也体现出你的经验)
使用icon 💡 ⭐ 🌟 ❓ 🚩🙋❗️
一、首先我们先来了解一下两者基本信息:
dom.onclick:onclick 事件会在元素被点击时发生,可以在 HTML 和 JavaScript 中使用,所有的浏览器都支持 onclick 事件。
onclick 属性可以使用与所有 HTML 元素,除了 :< base > , < bdo >, < br >, < head >, < html >, < iframe >, < meta >, < param >, < script >, < style >, 和 < title >.
dom.addEventListener:document.addEventListener() 方法用于向文档添加事件句柄。具有三个参数,分别是事件,触发执行的函数,以及是否在在捕获或冒泡阶段执行
Internet Explorer 8 及更早IE版本不支持 addEventListener() 方法,Opera 7.0 及 Opera 更早版本也不支持。 但是可以使用 attachEvent() 方法来添加事件句柄
二、下面我们来讨论一下这三者的区别:
事件监听器(addEventListener / attachEvent)
<a href="//google.com">Try clicking this link.</a>
const element = document.querySelector('a');
element.addEventListener('click', event => event.preventDefault(
console.log("Hello World");
));
element.addEventListener('click', event => event.preventDefault(
console.log("How are you?");
));
element.click()
// "Hello World"
// "How are you?"
内联事件监听器
<a href="//google.com" onclick="event.preventDefault();">Try clicking this link.</a>
相当于内联 JavaScript
<a href="//google.com">Try clicking this link.</a>
const element = document.querySelector('a');
element.onclick = event => event.preventDefault1();
element.onclick = event => event.preventDefault2();//会覆盖上面的执行函数
相比上面的方式,基本类似,但因为相当于内联JavaScript,也就是我们是在编写脚本,而不是HTML,就使得我们可以定义实现的更多,也就是可以使用匿名函数、函数引用和/或闭包。但这种方式存在被覆盖的可能性,也就是我们定义的可能被覆盖
三、总结
总的来说没有说那种是最好的,最好的往往是根据实际的情况选择的,但是一般建议使用 addEventListener,因为它可以做任何事情 onclick 能做的,甚至更多。但是可以说 onclick 基本是始终都会有效的,而 当我们addEventListener 需要考虑是否支持。
并且不要使用内联 onclick 来用作 HTML 属性,因为这会混淆 javascript 和 HTML,这是一种不好的做法,它使得代码的可维护性降低。应该说如果没有很好的理由,所有的内联形式都是不建议的。
一、逻辑与 && 和 逻辑或 ||
我们都知道当我们操作数是布尔值的时候,逻辑与需要操作都为 true 才会返回 true,而逻辑或只需要操作数中有一个为 true 就会返回 true, 但是要是我们的操作数不是 布尔值呢?
二、你们猜猜下面的输出是多少吗?
console.log(0&&1) //0
console.log(1&&2) //2
console.log(0||1) // 1
console.log(1||2) //1
首先我们先看结论,下面我们再来分析为什么。
逻辑与操作符是一种短路操作符,也就是如果第一个操作数决定了结果,就永远不会去对第二个操作数求值逻辑或操作符也是一种短路操作符,但不通的是当第一个·操作符为 true ,就不会对第二个操作符求值好吧,说人话:
根据上面四条我们就能知道为什么啦!然后需要补充的是在 在 JavaScript 逻辑运算中,0、”“、null、false、undefined、NaN都会判为false,其他都为true
三、那么这短路的特性有什么作用呢?
let myObject = perferredObject || backupObject
{{ data.owner && data.owner.avatar }}
说到作用域链,我们不得不先从作用域开始。首先我们得知道在js中有全局作用域和函数作用域。顾名思义:
作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
全局作用域的变量,函数在整个全局中都能被访问到,它的生命周期和页面的等同
函数作用域的,只能在当前函数内被访问到,生命周期随函数结束而结束销毁。
所以每一个变量或函数都会有自己的作用域范围,而作用域链简单看来说就是:
当前作用域范围(自身内部)中找不到时,就会往他的上一级寻找有没有,直到全局都没有的,返回 undefined。
要小心的是,有些时候,不要相信我们第一眼看到的就以为是他的上一级。如何判断他的上一级需要根据词法作用域来判断。
一、JS 数据类型有:
数字(number)、字符串(string)、布尔(bool)、符号(symbol)、空(undefined)、空(null)、对象(object)、大整型(bigint)。
二、JS 数据类型又可以分为两种
基本数据类型:包括Undefined、Null、Boolean、Number、String、Symbol、BigInt七种基本数据类型。
引用数据类型:Object。常见的有对象、数组和函数等。
三、基本数据类型和引用数据类型有什么区别?
概念上:
存放位置:
四、真值和假值
这里再说多个知识点,那就是真值和假值
假值:
真值:
注意❗️❗️❗️
在判断是要注意区分开了,自己判断的是某个字变量(null,undefined…)还是由于假值的影响
一、常见判断:typeof
判断基本数据类型,如:string,number,boolean,symbol,bigint(es10新增一种基本数据类型bigint),undefined等。返回数据类型的字符串形式typeof 目前能返回string,number,boolean,symbol,bigint,unfined,object,function这八种判断类型,但是注意 null 返回的是 Object 。而且对于引用类型返回的是 object 因为所有的对象的原型最终都是 Object。
//例子
1 typeof "safsadff"; //"string"
2 typeof 145; //"number"
3 typeof false; //"boolean"
4 typeof undefined; //"undefined"
5 typeof function(){}; //"function"
6 typeof {}; //"object"
7 typeof Symbol(); //"symbol"
8 typeof null; //"object"
9 typeof []; //"object"
10 typeof new Date(); //"object"
11 typeof new RegExp(); //"object"
12 typeof new Number(33) //"object"
13 typeof Null //"undefined"
面试题
为什么typeof null 是 Object
答:因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object
这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:
- 000 对象
- 1 整型
- 010 双精度类型
- 100字符串
- 110布尔类型
二、 已知对象判断:instanceof
判断引用数据类型的,判断基本数据类型无效 ,如:Object,Function,Array,Date,RegExp等,instanceof 主要的作用就是判断一个实例是否属于某种类型//例子
1 [] instanceof Array; // true
2 [] instanceof Object; // true
3 [1,2,3] instanceof Array // true
4 new Date() instanceof Date // true
手写实现一个:
//手写实现
function myInstance(L, R) {//L代表instanceof左边,R代表右边
var RP = R.prototype
var LP = L.__proto__
while (true) {
if(LP == null) {
return false
}
if(LP == RP) {
return true
}
LP = LP.__proto__
}
}
console.log(myInstance({},Object));
三、根据对象的构造器:constructor
与 instanceof 相似,但是对于 instanceof 只能再检测引用类型, 而 constructor 还可以检测基本类型,因为 constructor是原型对象的属性指向构造函数。
注意问题
- null 和 undefined是无效的对象,因此是不会有 constructor 存在的,所以无法根据 constructor 来判断。
- JS对象的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写prototype 后,原有的 constructor 会丢失,constructor 会默认为Object
- 类继承的也会出错,因为 Object 被覆盖了,检测结果就不对了
四 、对象原型链判断:Object.prototype.toString.call(这个是判断类型最准的方法)
toString 是Object 原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object,xxx],xxx是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument… 基本上所有对象的类型都可以通过这个方法获取到。
必须通过 Object.prototype.toString.call 来获取,而不能直接 new Date().toString(), 从原型链的角度讲,所有对象的原型链最终都指向了Object,按照JS变量查找规则,其他对象应该也可以直接访问到Object的 toString 方法,而事实上,大部分的对象都实现了自身的 toString 方法,这样就可能会导致 Object 的 toString 被终止查找,因此要用 call 来强制执行 Object 的 toString 方法。
缺点:不能细分为谁谁的实例
//例子
1 Object.prototype.toString.call("a")
"[object String]"
2 Object.prototype.toString.call(undefined)
"[object Undefined]"
3 Object.prototype.toString.call(null)
"[object Null]"
4 Object.prototype.toString.call(new Date())
"[object Date]"
面试回答🙋♂️🙋♂️🙋♂️
简单来说,JavaScript 中我们有四种方法来判断数据类型。
一般使用 typeof 来判断基本数据类型,不过需要注意当遇到null的问题,这里不足就是不能判断对象具体类型(typeof xjj 只是Object不能看出是person);
而在要判断一个对象的具体类型,就可以用 instanceof,但是也可能不准确,对于一些基础数据类型,数组等会被判断为object。
结合 typeof 和 instanceof 的特点,还能使用 constructor 来判断,他能判断基本类型和引用类型,但是对于 null 和 undefined 是无效的,以及 constructor 不太稳定。
最后如果需要判断准确的内置类型,就可以使用 object.prototype.toString.call,是根据原型对象上的 tostring 方法获取的,该方法默认返回其调用者的具体类型。
conlog.log("你好呀大笨蛋".length)
// 6
我们知道中文在数据库中存放是占两个字符的,但是在浏览器中,由于 javascript 是 unicode 编码的,所有的字符对于它来说一个就是一个,获取的是中文的长度而不是字符的长度,所以上面的输出是 6 。
这就会导致前后端的对于中文的验证长度不一样了,如何解决?
function getRealLength( str ) {
return str.replace(/[^\x00-\xff]/g, '__').length; //这个把所有双字节的都给匹配进去了
}
NaN 属性是代表非数字值的特殊值。该属性用于指示某个值不是数字。可以把 Number 对象设置为该值,来指示其不是数字值。
NaN 属性是一个不可配置(non-configurable),不可写(non-writable)的属性。但在ES3中,这个属性的值是可以被更改的,但是也应该避免覆盖。
编码中很少直接使用到 NaN。通常都是在计算失败时,作为 Math 的某个方法的返回值出现的(例如:Math.sqrt(-1))或者尝试将一个字符串解析成数字但失败了的时候(例如:parseInt(“blabla”))。
typeof NaN; // “number”
NaN 的特性
使用 isNaN() 函数来判断一个值是否是 NaN 值。
有些时候傻傻的感觉两者一样但又不一样,有些时候要判断一个值是否为空,不知道选哪个好,感觉两者都一样,这里去看看两者异同吧~
一句简单来说是:undefined 代表了不存在的值,null 代表了没有值。
也就是,比如我们对一个值声明后,没有赋值,输出他就是 undefined ,是不存在的,而当我们赋值为 null, 那么输出他就是 null。
这两者还有一些区别点要注意的:
1.
undefined == null //true,这里相等是因为 “==” 对他们做了转换
undefined === null //false
//undefined的值 是 null 派生来的,所以他们表面上是相等的
2.
let a;
typeof a;//undefined
let b= null;
typeof b;//object
这里为什么typeof b 输出为 Object 呢?
null 不是一个对象,尽管 typeof age输出的是 Object,逻辑上讲, null 值表示一个空对象指针 ,这是一个历史遗留问题,JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,null表示为全零,所以将它错误的判断为 Object 。
Undeclared(未声明) : 当尝试访问尚未使用 var、let 或 const 声明的变量时会发生这种情况。
Undefined(未定义) : 它发生在使用 var、let 或 const 声明变量但未赋予值时。
在 JavaScript 中,两者很容易被混为一谈
typeof 对 undefined 和 undeclared 的 都返回 “undefined”
let a;
typeof a; // undefined
typeof b // undefined (实际上这里应该输出 undeclared 才是合适的)
我们访问 undeclared 变量的时候,是这样报错的:ReferenceError : a is not defined。
var a;
console.log(a); //undefined
console.log(b); //ReferenceError: b is not defined
其实这两个对于很多人都了解,但是可能对于一些新手就没有了解到两者的区别
他们最主要的区别在于 **==** 对比时,若类型不相等,会先转换为相同类型,然后再来比较值。而 **===** 则不会,只能在相同类型下比较值,不会做类型转换。还有一个是 **=** ,这个是赋值,不是运算符。
1、 ===
这个比较简单。下面的规则用来判断两个值是否===相等:
2.、==
还有我们有些时候会在结构读取属性值的时候 会在前面加上 “?” 这个是可选链,表示判断空值的情况。
我们看下面的输出:
console.log(0.1 + 0.2) // 结果是0.30000000000000004,而不是3
那么为什么不是呢~主要是因为在 JavaScript 中,数字是采用的IEEE 754的双精度标准进行存储。比如其中小数使用64位固定长度来表示的,其中的1位表示符号位,11位用来表示指数位,剩下的52位尾数位。
而其中,比如 0.1 转换为二进制是一个无限循环的数的,就会超过 52 位,就会导致精度缺少,所以在计算机中 0.1 只能存储成一个近似值
所以同理的, 0.1 + 0.2 会先分别转换为二进制然后进行相加后存储,最后取出来的时候转化为十进制,而这个值不够近似于0.3,所以就会出现不相等的结果。
那么如何避免呢?
常用简单的办法就是将浮点数转变为整数来计算。

一、静态语言
二、动态语言
三、弱/强类型语言
四、JavaScript
JavaScript 是一种弱类型的、动态的语言。那这些特点意味着什么呢?
前几天用到的获取时间的一些基本方法,这里简单介绍,举个获取当天时间的例子:
//获取当前时间
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
//这两个是针对小于十的时候,会为个位数
if (month < 10) {
month = "0" + month;
}
if (day < 10) {
day = "0" + day;
}
var nowDate = year + "-" + month + "-" + day;
//往往格式成这种样式是后台处理好的,或者我们会使用一些插件,又或者我们在vue中会使用过滤器来处理。
//输出结果格式:2021-11-08
一、基本的函数:
二、获取时间戳的方法
let timestamp =(new Date()).valueOf();
//或者
let timestamp=new Date().getTime();
三、一道练习题
问题 :返回 “现在距离 今年元旦 还有 X 天 X 小时 X 分钟 X秒”
function timemove() {
// 获取当前时间
let d1 = new Date();
//获取下一年
let nextYear = d1.getFullYear() +1;
// 定义目标时间
let d2 = new Date( nextYear +"/1/1 00:00:00");
// 定义剩余时间
let d = d2 - d1; // 是一个时间戳
// 计算剩余天数
let Day = parseInt(d / 1000 / 60 / 60 / 24);
// 计算剩余小时
let Hours = parseInt(d / 1000 / 60 / 60 % 24);
// 计算剩余分钟
let Minutes = parseInt(d / 1000 / 60 % 60);
// 计算剩余秒
let Seconds = parseInt(d / 1000 % 60);
//拼接变量
let time = Day + "天" + Hours + "小时" + Minutes + "分钟" + Seconds + "秒";
// 将拼接好的变量显示在页面
console.log("距离元旦还有" + time )
}
推荐阅读:关于时间的一些时间方法获取:
请问下面这个结果是多少呢?
123['toString'].length + 123 = ?
1、形参个数
咱们来看看下面这个例子
function fn1 () {}
function fn2 (name) {}
function fn3 (name, age) {}
console.log(fn1.length) // 0
console.log(fn2.length) // 1
console.log(fn3.length) // 2
可以看出,function有多少个形参,length就是多少。但是事实真是这样吗?继续往下看
2、 默认参数
如果有默认参数的话,函数的length会是多少呢?
function fn1 (name) {}
function fn2 (name = '林三心') {}
function fn3 (name, age = 22) {}
function fn4 (name, aaa, age = 22, gender) {}
function fn5(name = '林三心', age, gender, aaa) {}
console.log(fn1.length) // 1
console.log(fn2.length) // 0
console.log(fn3.length) // 1
console.log(fn4.length) // 2
console.log(fn5.length) // 0
上面可以看出,function的length,就是第一个具有默认值之前的参数个数
3、 剩余参数
在函数的形参中,还有剩余参数这个东西,那如果具有剩余参数,会是怎么算呢?
function fn1(name, ...args) {}
console.log(fn1.length) // 1
可以看出,剩余参数是不算进length的计算之中的
4、总结
总结之前,先公布123[‘toString’].length + 123 = ?的答案是124
总结就是:length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
我们可以通过 event.bubbles 属性可以判断该事件是否可以冒泡
| 事件 | 是否冒泡 |
|---|---|
| click | 可以 |
| dbclick | 可以 |
| keydown | 可以 |
| keyup | 可以 |
| mousedown | 可以 |
| mousemove | 可以 |
| mouseout | 可以 |
| mouseover | 可以 |
| mouseup | 可以 |
| scroll | 可以 |
概括来说,鼠标事件,和键盘事件,以及点击事件是支持冒泡的
| 事件 | 是否冒泡 |
|---|---|
| blur | 不可以 |
| focus | 不可以 |
| resize | 不可以 |
| about | 不可以 |
| mouseenter | 不可以 |
| mouseleave | 不可以 |
| load | 不可以 |
| unload | 不可以 |
而聚焦和失焦事件,加载事件,ui事件、鼠标移入移出事件是不支持的。
一、构造函数
在 JavaScript 中,通过 new 来实例化对象的函数叫构造函数,也就是初始化一个实例对象,对象的 prototype 属性是继承一个实例对象。构造函数的命名一般会首字母大写~
二、 那么为什么需要使用构造函数呢?
是为了创建对象
而 JavaScript 中创建对象有两种,一种是 构建函数 + prototype,另一种是用 class。这里我们不去讲解 class ,先放到构造函数上。
//当我们需要创建比较多信息时
var person1 = { name: 'aa', age: 6, gender: '男', classRoom:'高一'};
var person2 = { name: 'bb', age: 6, gender: '女', classRoom:'高一'};
var person3 = { name: 'cc', age: 6, gender: '女', classRoom:'高一'};
var person4 = { name: 'dd', age: 6, gender: '男', classRoom:'高一'};
那么如果我们需要创建很多呢?需要这样一个一个的写下去吗?但是实际上我们可以通过下面的形式来实现。
function Person(name, age, gender, classRoom) {
this.name = name;
this.age = age;
this.gender = gender;
this.classRoom= '高一'
}
Person.prototype.sayHi = function() {
console.log("你好,我叫" + this.name + "是一个" + this.sex + "来自"+ classRoom);
};
let person1 = new Person("a", 18, '男');//我们还可以不传 classRoom,让他使用默认的
let person2 = new Person("b", 19, '女');
三、构造函数的执行过程
构造函数的执行过程其实也就是 new 操作付的基本过程
new 操作符通过构造函数创建的实例,可以访问构造函数的属性和方法,同时实例与构造函数通过原型链连接起来了。
四、构造函数的返回值
首先说的,构造函数中,不要显式返回任何值,下面我们看看为什么:
//返回原始值
function Person(name) {
this.name = name;
return '啦啦啦啦'
}
let person1 = new Person("a");
console.log(person1.name) //a
//返回对象
function Person(name) {
this.name = name;
return {aaa : "asdas"}
}
let person1 = new Person("a");
console.log(person1) //{aaa : "asdas"}
console.log(person1.name) //'undefined'
可以看到当返回原始值的时候,并不会正常返回这个原始值 “啦啦啦啦”,而当返回直是对象的时候,这个返回值能被正常返回,但是这时候 new 就不生效了。所以,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。
那么为什么会这样呢?
构造函数没有返回值的原因是因为它不是由你的代码直接调用的,它是由运行时的内存分配和对象初始化代码调用的。 它的返回值(如果它在编译为机器代码时实际上有一个)对用户来说是不透明的——因此,你不能指定它。
其实在 JavaScript 中,
- let a = [] 也就是 let a = new Array [];
- function a () {} 也就是 let a = new Function () {}
上面我们说了构造函数,下面我们来看看原型和原型链的,因为这两者就是为了解决构造函数还有的问题的。同时解释上文中说道的使用 new 构造函数是 “初始化一个实例对象,对象的 prototype 属性是继承一个实例对象”
一、那么构造函数还有什么问题呢?
构造函数的最主要问题在于,其定义的方法会在每一个实例上都创建一遍。 什么意思呢?我们看下面的例子:
function Person(name) {
this.name = name;
this.age = 18;
this.say = function () {
console.log("你好,我今年" + this.age);
}
}
let person1 = new Person("a");
let person2 = new Person("b");
console.log(person1.say == person2.say) //false
我们可以看到 每个person 实例都有自己的 function 实例,不同的 function 实例,尽管同名但也不相等。而这里大家做的都是同一件事,其实完全没有必要定义两个不同的 function 实例了。而为了创建重复的对象,同时又能保证性能,就引出了我们的猪脚:原型模式。(所到底,构造函数和原型都是为了创建对象~)
二、原型模式
首先我们先来看一段代码,然后我们分析这段代码做了什么,最后我们来看看这段代码中出现了那些角色,然后我们来看看每一个角色的作用。
function Person(name) {
Person.prototype.name = name;
Person.prototype.age = 18;
Person.prototype.say = function () {
console.log("你好,我今年" + this.age);
}
}
let person1 = new Person("a");
let person2 = new Person("b");
console.log(person1.say == person2.say) //true
在这里,因为使用原型模式定义的属性和方法是由所有的实例所共享的,所以 person1 和 person2 访问的都是相同的 say ( ) 函数。
下面我们来了解一下原型模式是如何实现的,解决构造函数的问题的

我们先记住两句话 ( 来自望远镜 ),然后我们来分析上图
只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。在自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自Object。每次调用构造函数创建一个新实例,这个实例的内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。比如上图我们的构造函数只要创建后,就会有一个 prototype 属性指向它的原型对象,而这个原型对象也有一个 constructor 属性指向构造函数,而构造函数的实例中,有一个 [[Prototype]] 指向 原型对象。这里的 [[Prototype]] 其实也就是 _proto _属性。
接下来我们来看看每一个的作用和特性:
1、prototype属性
该属性是 构造函数 独有的,用于指向原型对象。而原型对象的用途是所有实例对象所共享的属性和方法。
2、[[ prototype ]]属性 ( _ proto _ )
该属性是 构造函数的实例 所独有的,用于指向构造函数的原型对象。也因为这个指向,所以实例中可以读取原型中所共享的属性和方法。
属性 [[Prototype]] 是内部的而且是隐藏的,我们可以使用特殊的名字 __proto__来访问他
3、constructor 属性
constructor属性是位于原型对象上的,用来指向创建对象的函数。因为所有的实例都能访问 constructor ,所以可以使用 constructor 属性来验证实例的原型类型
三、继承
当我们访问实例的一个属性或者调用一个方法,但它不存在,那么 JavaScript 就会尝试在原型中查找它。
let animal = {
eats: “草”
};
let rabbit = {
jumps: true,
__proto__: animal
};
console.log(rabbit.eats) //草
我们可以看到在 rabbit 这里中,并没有 eats 这个属性,但是我们给 rabbit 的 __ proto __绑定到 animal 中,也就是说, rabbit 可以通过 __ proto __ 来访问到 animal 的原型对象,就能读取到 eats 这个属性了。
首先 JavaScript 引擎会先在实例对象本身中搜索有没有,查找到了就返回,如果没有,就会顺着 __ proto __ 进去到原型对象中去查找。而如果两者都有一个同名的属性,那么实例上的这个属性会遮盖原型对象上的。
我们常常用到 freeze 来冻结对象,那么 freeze 属性是如何实现冻结对象,使得不得修改的呢?
一、这里是关于到属性的四个基本类型
1、[[Configurable]]:
表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为 true 。
2、[[Enumerable]]:
表示属性是否可以通过for-in循环返回。默认为 true 。
3、 [[Writable]]:
表示属性的值是否可以被修改。默认为 true 。
4、[[Value]]:
表示属性实际的值。默认为 undefined 。
当我们显式的添加一个属性之后,它的值就会被设置到 [[Value]] 中,而 [[Writable]]表示我们能否读这个值进行修改,如果设置为 false ,那么当我们修改这个值的时候回就会报错,同理其他的属性也是一样的。而如果我们要修改属性的类型的值的时候 ,是使用 Object.defineProperty()方法 来进行修改的。
二、那么 freeze 做了什么?
那么回到我们的前面开篇的问题,当我们使用了 feeeze 的时候,它就会做下面的事情:
我们有时候会看到有一些属性是用两个中括号包含起来的,这是怎么回事呢?这是因为:
当为了将某个特征标识为内部特性,规范会用两个中括号把特性的名称括起来
一、会改变原数组的操作
1、pop ( )
pop() 方法用于删除数组的最后一个元素并返回删除的元素。
2、push ( )
push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。
3、shift ( )
shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
4、unshift ( )
unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
5、reverse ( )
reverse() 方法用于颠倒数组中元素的顺序。
6、sort ( )
sort() 方法用于对数组的元素进行排序。排序顺序可以是字母或数字,并按升序或降序。默认排序顺序为按字母升序。
7、splice ( ):
splice() 方法用于添加或删除数组中的元素。
二、不会改变原数组的操作
1、concat ( )
concat() 方法用于连接两个或多个字符串。该方法没有改变原有字符串,但是会返回连接两个或多个字符串新字符串。
2、join ( )
join() 方法用于把数组中的所有元素转换一个字符串。元素是通过指定的分隔符进行分隔的。
3、slice ( )
slice() 方法可从已有的数组中返回选定的元素。slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
4、filter ( )
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
5、reduce ( )
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
6、find ( )
find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。
find() 方法为数组中的每个元素都调用一次函数执行:当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。
如果没有符合条件的元素返回 undefined
7、findIndex ( )
findIndex() 方法返回传入一个测试条件(函数)符合条件的数组第一个元素位置。
findIndex() 方法为数组中的每个元素都调用一次函数执行:
当数组中的元素在测试条件时返回 true 时, findIndex() 返回符合条件的元素的索引位置,之后的值不会再调用执行函数。
如果没有符合条件的元素返回 -1
三、归纳图
没有什么能比一张图更能让人们一目了然了,下面看图吧!(作图软件没得修改,只能这样,感觉不大好看,不过实用就好了~)

说道遍历的方法我们知道的就多了,但是如果我们说是遍历对象的方法呢?首先我们想到的肯定是 for...in 那么除了这种还有别的吗?奶奶的,上次面试就被问了这个,一下子麻了,好像就除了这个就没有认识过别的了~废话不说了,下面看看遍历对象有啥方法吧!
for in// Object 原型链上扩展的方法也会被遍历出来
Object.prototype.fun = () => {};
var obj = {a:1, b:2, c:3};
for (const item in obj) {
console.log("属性名:" + item+ " / 属性值:" + obj[item]);
}
// 属性名:a / 属性值:1
// 属性名:b / 属性值:2
// 属性名:c / 属性值:3
// 属性名:fun / 属性值:() => {}
//而如果我们不希望搜索到原型上的,我们就可以使用 hasOwnProperty
for (const item in obj) {
if (obj.hasOwnProperty(item)) {
console.log("属性名:" + item+ " / 属性值:" + obj[item]);
}
}
// 属性名:a / 属性值:1
// 属性名:b / 属性值:2
// 属性名:c / 属性值:3
object.key// 注意:当属性名为数字的时候会排序,key / values / entries都有这个特性~
const anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // console: ['2', '7', '100']
const str = 'hello';
console.log(Object.keys(str));// ["0", "1", "2", "3", "4"]
const arr = ['a', 'b', 'c'];
console.log(Object.key(arr));// ["0", "1", "2"]
var obj = {a:1, b:2, c:3};
console.log(Object.keys(obj));// ["a","b","c"]
Object.values// 注意:当属性名为数字的时候会排序,key / values / entries都有这个特性~
const obj = {100:1, d:2, a: 9, 1:3, 5: 99 , b: 8};
console.log(Object.values(obj)); // [3,99,1,2,9,8]
const str = 'hello';
console.log(Object.values(str));// ["h", "e", "l", "l", "o"]
const arr = ['a', 'b', 'c'];
console.log(Object.values(arr));// ["a", "b", "c"]
var obj = {a:1, b:2, c:3};
console.log(Object.values(obj));// ["1","2","3"]
Object.entries// 注意:当属性名为数字的时候会排序,key / values / entries都有这个特性~
const obj = {100:1, d:2, a: 9, 1:3, 5: 99 , b: 8};
console.log(Object.entries(obj)); // [["1",3],["5",99],["100",1],["d",2],["a",9],["b",8]]
const str = 'hello';
console.log(Object.entries(str));// [["0","h"],["1","e"],["2","l"],["3","l"],["4","o"]]
const obj = {a:1, b:2, c:3};
console.log(Object.entries(obj));// [["a",1],["b",2],["c",3]]
const obj1 = {a:1, b:2, c:3};
for (const [key, value] of Object.entries(obj1)) {
console.log(`${key}: ${value}`);
}
//a: 1
// b: 2
// c: 3
Object.getOwnPropertyNames// 注意:当属性名为数字的时候会排序,key / values / entries都有这个特性~
const arobj = {100:1, d:2, a: 9, 1:3, 5: 99 , b: 8};
console.log(Object.getOwnPropertyNames(arobj)); // ["1","5","100","d","a","b"]
const str = 'hello';
console.log(Object.getOwnPropertyNames(str));// ["0","1","2","3","4","length"]
const arr = ['a', 'b', 'c'];
console.log(Object.getOwnPropertyNames(arr));// ["0","1","2","length"]
const obj = {a:1, b:2, c:3};
console.log(Object.getOwnPropertyNames(obj));// ["a","b","c"]
const syobj = { a:1, b:2 };
const symbol1 = Symbol('symbol1')
const symbol2 = Symbol('symbol2')
syobj[symbol1] = 'hello'
syobj[symbol2] = 'world'
console.log(Object.getOwnPropertyNames(syobj)); //["a","b"]
Object.getOwnPropertySymbols()const obj = {a:1, b:2, c:3};
const symbol1 = Symbol('symbol1')
const symbol2 = Symbol('symbol2')
obj[symbol1] = 'hello'
obj[symbol2] = 'world'
console.log(Object.getOwnPropertySymbols(obj));// [Symbol(symbol1), Symbol(symbol2)]
Reflect.ownKeys()const obj = {a:1, b:2, c:3};
const symbol1 = Symbol('symbol1')
const symbol2 = Symbol('symbol2')
obj[symbol1] = 'hello'
obj[symbol2] = 'world'
console.log(Reflect.ownKeys(obj));// ["a","b","c",Symbol(symbol1), Symbol(symbol2)]

注意❗❗❗
上面我们看代码中有个问题是就是当 Object 中 key 为数字时输出并不是按顺序的,难道 Object 的键值对是无序的?
直接看结论吧!
- 在 ES6 之前 Object 的键值对是无序的;
- 在 ES6 之后 Object 的键值对按照自然数、非自然数和 Symbol 进行排序,自然数是按照大小升序进行排序,其他两种都是按照插入的时间顺序进行排序。
一句话来说就是:for ... in是为遍历对象属性而构建的,遍历的是 index,而 for ... of是为了遍历数组的,遍历的是 value
const arobj = {100:1, d:2, a: 9, 1:3, 5: 99 , b: 8};
Object.prototype.fun = () => {};
arobj.name="lallala"
const arr = [1,2,4,8,9,10]
Array.prototype.method= () => {};
arr.name="bababa";
for(const item in arobj) {
console.log(arobj[item]) // 3 99 1 2 9 8 lallala () => {}
}
for(const item in arr) {
console.log(item) //0 1 2 3 4 5 name method fun
}
for(const item of arobj) {
console.log(item) // Uncaught TypeError: arobj is not iterable
}
for(const item of arr) {
console.log(item) // 1 2 4 8 9 10
}
一、for … in
二、for … of
每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置
我们如何判断 this 的指向问题呢?有两个关键的点 调用位置 和 绑定规则。下面我们来理解如何通过这两个来判断 this 的指向!
一、调用位置
这个倒是没有什么难得地方,我们知道我们的执行是在执行栈中的,执行栈是一个栈的结构,遵从先进后出,我们会把执行的任务压入栈中。所以我们这里寻找的调用位置就是当前正在执行的函数的上一个调用的位置。下面看个例子理解:
function a() {
//当前调用栈是:a,所以当前调用位置是全局
console.log(“a”)
b(); // b 的调用位置
}
function b() {
//当前调用栈是:a->b,所以当前调用位置是a
console.log(“b”)
c(); // c 的调用位置
}
function c() {
//当前调用栈是:a->b->c,所以当前调用位置是b
console.log(“c”)
}
a()
二、绑定规则
默认绑定:这条规则可以看作是无法应用其他规则时的默认规则,也是我们最常用的。var a = 2
function foo() {
var a = 3;
console.log(this.a)
}
foo() // 2,为什么不是3,那就是因为使用了 this,根据我们上面的调用规则,他是绑定到全局中的
隐式绑定var a = 1;
function foo() {
var a = 4;
console.log(this.a)
}
var obj2 = {
a:2,
foo:foo
};
var obj1 = {
a:3,
obj2: obj2
};
obj2.foo() // 2 当函数拥有上下文对象时,隐式绑定会把this绑定到这个上下文来
obj1.obj2.foo() //2 如果函数调用前存在多个对象,this指向距离调用自己最近的对象
在隐式绑定中,还会出现一个问题,那就是 隐式丢失,作为 参数传递 以及 变量赋值, 会使参数或者变量直接指向函数,从而丢失 this 指向。下面我们看看这是啥!
1. 变量赋值
var a = 1;
function foo() {
var a = 3;
console.log(this.a)
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;
bar() // 1
为什么这里输出的是 1 ,而不是按照上面的规则输出 2 呢?这是因为这里的引用实际只是给函数起了个名字 bar 而已,并没有被调用,在声明 var bar 后才调用 bar () == foo (), 而此时的 this 指向也就是全局的了。
2. 参数传递
var a = 1;
function foo() {
var a = 3;
console.log(this.a)
}
function doFun(fn) {
fn()
}
var obj = {
a:2,
foo:foo
};
doFun(obj.foo;) // 1
这里其实也是和上面的情况相似的,在 doFun 里面,fu 其实引用的是 foo,所以指向的是全局的
那么如何解决 隐式丢失 呢?
使用 隐式绑定 :将函数绑定至对象属性,调用时通过对象属性直接调用,弱赋值到其他对象,需将正对象赋值过去,不然会发生丢失(丢失初次绑定的环境)
显式绑定显示绑定就是指通过 call,apply 以及 bind方法 改变 this 的行为,具体这三个方法看下面就好了~
new 绑定我们在上面的构造函数中讲到 new 操作符构造函数的过程,可以上去看看~
使用 new 来调用的时候,实际构建一个新的对象并把他绑定到 foo()调用的 this 上。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a) // 2
箭头函数这个我也不再这里多讲了,在本篇中有介绍过箭头函数的 ,这里简单总结就是
因为箭头函数不会创建自己的 this,所以它没有自己的this,它只会从自己的作用域链的上一层继承 this。
首先这三者的作用都是用来重新定义 this 这个对象的!
一、下面我们看看这三者的区别:
1、调用上
let name = "www", age = "17";
let obj = {
name = "aaa";
objAge = this.age;
myFun: function() {
console.log( this.name + "年龄" + this.age)
}
}
let db={
name: "bbb";
age: 12
}
obj.myFun.call(db); // bbb年龄 99
obj.myFun.apply(db); // bbb年龄 99
obj.myFun.bind(db)(); // bbb年龄 99
首先我们可以看出,除了 bind 需要在方法后面添加 “()” 以外,其他的都是直接调用。这是因为 bind()方法创建了一个新的函数,你必须调用显函数才会执行目标函数, 而对于 call 和 apply 是使用后马上执行。
2、参数上
let name = "www", age = "17";
let obj = {
name = "aaa";
objAge = this.age;
myFun: function(fm, t) {
console.log( this.name + " 年龄" + this.age,"来自" + fm + "去往" + t)
}
}
let db={
name: "bbb";
age: 12
}
obj.myFun.call(db,'成都','上海'); // bbb 年龄 99 来自 成都去往上海
obj.myFun.apply(db,['成都','上海']); // bbb年龄 99 来自 成都去往上海
obj.myFun.bind(db,'成都','上海')(); // bbb 年龄 99 来自 成都去往上海
obj.myFun.bind(db,['成都','上海'])(); // bbb 年龄 99 来自 成都, 上海去往 undefined
可以看出,三个函数的第一个参数都是 this 的指向对象,区别在于第二个参数。
Function.prototype.call 来说,第一个参数就是 this 的指向对象,其余参数是直接放进去,用逗号隔开就好了。Function.prototype.apply 来说,Function.apply( obj,args ) 方法能接收两个参数,obj:是 this 的指向对象。而 args:这个是数组,它将作为参数传递,也就是说 apply 的所有参数都必须放在一个数组里面传进去Function.prototype.bind 来说,第一个参数就是 this 的指向对象,其余参数是直接放进去,用逗号隔开就好了。也就是说他和 call 是基本相同的,除了是返回是一个函数。注意❗❗❗
对于 bind 来说,多次的 bind 调用,this的指向仍然是第一次的
function aa() {
console.log(this)
}
aa.bind(1).bind(2)() // 1
二、bind / call / apply 的异同
相同:都能改变 this 的指向,都是挂载在 Function. prototype 上
不同:call 和 apply 是使用后马上执行,而 bind 是返回一个新的函数,调用显函数才会执行目标函数。并且 call 和 bind 的参数格式是一样的,第一个参数是 this 的指向对象,其余参数用逗号,apply 是参数需要放到数组中。
面试回答 🙋♂️🙋♂️🙋♂️
关于修改 this 的指向的方法有三个, bind 和 call 以及 apply,他们的相同点都是 能修改 this 的指向的问题的,并且都是挂载在 Function.prototype 上的。
不同点在于参数 和执行上,call 和 bind 的参数格式是一样的,第一个参数是 this 的指向对象,其余参数用逗号,而 apply 的参数需要放到数组中。在执行中,call 和 apply 是使用后马上执行,而 bind 是返回一个新的函数,调用显函数才会执行目标函数。
其中需要注意的是,箭头函数的 this 是指向他所在的上下文中,并且是不能使用这三个方法修改的。
我们常听闭包。那么什么是闭包呢?
一、定义
闭包是由函数以及声明该函数的词法环境组合而成的,也就是在一个内层函数中你可以访问到其外层函数的作用域
比如当通过调用一个外部函数返回一个内部函数后,而当该外部函数已经执行结束了,但是因为内部函数引用外部函数的变量,所以就会出现一个空间使得这些变量依然保存在内存中,我们就把这些变量的集合就称为闭包。如下面的例子:
function add() {
var a = 10;
function show() {
console.log(a)
}
return show
}
var adds = add()//这行相当于var adds = show()
adds()
在这里调用add(),结束了,返回show(),按道理add()已经结束了弹出栈,所以局部变量a 的值应该访问不了,但实际上是可以的,这是为啥呢?就是我们前面说的闭包,下面通过一张图你就能很好的理解了。(注意闭包在栈空间存放的是一个访问地址,实际存放在堆空间)

二、那么探索闭包存放的位置
我们创建一个非常大的 变量a 和 变量b 我们可以发现在堆空间里发现 变量a ,那么还有个问题,所有 函数add()里的变量都会存放到闭包吗?我们通过下面的的图也能看出,只有被内部函数引用的变量才会加入到闭包里。
function add() {
var a = new Array(1000000).join('a');
var b = new Array(1000000).join('b');
function show() {
console.log(a)
}
return show
}
var adds = add()
adds()
执行后在对空间里的内存。

三、闭包的回收
因为闭包的性质,在根据垃圾回收的原则,只要被引用的就不会被回收,这样就很容易导致内存泄露。我们应该注意对闭包的使用。
所以我们原则上:闭包会一直使用的就创建为全局变量,反之为局部变量。
四、闭包的作用和使用场景
1、作用
2、场景
面试回答🙋♂️🙋♂️🙋♂️
Let 和 Const模板字符串箭头函数扩展运算符for...of 和 for...inSymbolMap 和 SetClassPromiseasync / await剩余参数Var 是最开始的 JavaScript 关键词之一,一个变量在 JavaScript 中,分为 声明 和 初始化。
Var 变量提升可以说是他的缺点了,美其名曰 JavaScript 特性。但实际上这个特性带来很多问题。无论声明在哪里,变量提升会把都会提升到该作用域的最顶部,你在任何地方都会访问的到。另外他是函数作用域,而且可以多次声明,就会造成你自己啥时候覆盖了都不知道
console.log(a) //undefien
var a = 1
console.log(a) //1
var a = 2
console.log(a) //2
另外
var a = 1 等价于 var a = undefined ,a = 1 两条语句合成,所以上面两行代码等价于下面
var a = undefined
console.log(a) //undefien
var a = 1
Let 和 Const 的出现就是为了解决 Var 的一些问题! Let 和 Const 具有下面的特点:
而其中 Const 和 Let 的最大区别是 Const 具有不可重新赋值的特性,其他地方都都类似Let 。
但有个地方需要注意的,当 Const 的值为引用类型时,是可以重新赋值的,因为引用类型在栈空间报存的其实是引用地址,真正的值保持在堆空间
那么问题来了,如何让引用类型的值也是不可更改呢?
这个问题看下面的一点吧~~
深化文章:JavaScript基础知识-变量
首先什么是暂时性锁区呢?
当我们使用 typeof 检测 或者输出一个未被声明的变量,实际上并不会不会报错,而是输出 undefined。这是为什么呢?是因为 “变量提升” 的存在?为什么变量提升会影响呢?
实际上我们的代码并不是就按照我们写的顺序所执行的,实际上存在一个预处理的步骤。也就是先对JavaScript代码进行编译,然后再执行。在编译的阶段就会先处理声明,处理作用域等。而没有声明的变量就会被提到作用域最上面声明,赋值为 undefined。
我们上面说过使用Let 有暂时性锁区,必须先声明后使用,避免了 Var 带来的变量提升的问题。这一点我们来深入看看暂时性锁区吧!
// name 会提升
console.log(name) // undefined
var name = 'aaa';
//age 不会提升
console.log(age) // ReferenceError:age 没有定义
let age = 26;
那么为什么使用 let 会报错呢?这是因为使用 var 会变量提升,但是使用 Let 存在块级作用域,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响,Let 和 Const 在声明前调用所,就读取不到了,报错了,语法上就叫做暂时性锁区(TDZ)。
// name 会提升
var name ; // name = undefied
console.log(name) // undefined
name = 'aaa';
// TDZ开始
age = 'abc'; // ReferenceError
console.log(age); // ReferenceError
let age; // TDZ结束
console.log(age); // undefined
tmp = 123;
console.log(age); // 123
上面一点我们聊过到最后留了一个问题,那就是我们都知道 Const 是一个必须声明的时候同时初始化且赋值的,并且这个值不能修改。
那么要是我们对他的赋值是一个引用类型呢?我们知道引用类型(比如对象,数组等)的话,这时候 Const 实际保持的是这个值在栈中的引用地址,实际的数据在堆里面保持存,所以这时候修改引用类型的值是不会违反 Const 的限制的。
那么问题来了~要是我们需要对这个值不可修改呢?即使他是引用类型?总共有三种方法:
第一种:Object.freeze
该方法可以冻结对象,被冻结的对象有下面的特性:
也就是该 对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。该属性常常用在我们从接口拿到的数据,为了避免发生了修改,会拿到后对级冻结。
freeze 做了什么 ?
第二种: Object.seal
该方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。与 Object.freeze 的不同在于对于 Object.seal 当前属性的值,只要原来是可写的就可以改
seal 做了什么?
第三种: Object. preventExtensions
让一个对象变的不可扩展,也就是永远不能再添加新的属性。
这几个函数有些时候傻傻分不清啥是啥,这里来一次全面的了解他们吧!
一、首先 JS函数有多少种定义的方式?
何为函数呢?简单来说就是重复执行的代码块,这样的一段 JavaScript 代码,它只定义一次,但可能被执行或调用任意次。
常见的定义方式有下面这几种
ps:第一种和第二种函数的定义的方式其实是第四种构造函数的语法糖,当我们定义函数时候都会通过 new Function 来创建一个函数,只是前两种为我们进行了封装,我们看不见了而已,js 中任意函数都是Function 的实例,比如下面:
- var arr = []; 为 var arr = new Array(); 的语法糖。
- var obj = {} 为 var obj = new Object(); 的语法糖
二、箭头函数和普通函数的区别
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target等,也不能用作构造函数。更适用于那些本来需要匿名函数的地方。
普通函数就万金油啦~~
| 箭头函数 | 普通函数 | |
|---|---|---|
| 写法 | 使用箭头定义,省去 function 关键词( => ) | function 关键词 |
| 可否具名 | 只能是匿名函数 | 可以具名也可匿名 |
| 构造函数 | 没有 prototype 属性,没办法连接它的实例的原型链,所以无法作为构造函数,也不能使用new | 可以用于构造函数,以此创建对象实例 |
| argument | 不绑定arguments,使用rest参数…(剩余参数语法)来访问参数列表 | 具有一个arguments对象,用来存储实际传递的参数 |
| this指向 | 没有 prototype ,所以本身没有this,一般指向其所在的上下文,任何方法都改变不了其指向,如 call() , bind() , apply() | this一般指向它的调用者 |
三、函数声明和函数表达式的区别
一般定义函数有两种的方式,就是函数声明和函数表达式,他们两者的最大区别在于函数声明会出现函数提升,提升到该函数作用域的最开头,所以无论在该作用域任意方位都能使用。
但是函数表达式并不会出现这种情况,所以函数表达式需要先赋值再使用(函数表达式也叫匿名函数),否则会由于变量声明提升,add === undefined。
//函数声明:
function add(a,b) {
return a +b
}
//函数表达式
let add = function(a,b) {return a+b}
add ()
//使用1
add (1,2) //报错,因为没有先赋值再使用
let add = function(a,b) {return a+b}
//使用2
add (1,2) //3
function add(a,b) {return a+b}
//使用3
if(ifTrue){
add(a,b) {return a+b}
}else {
add(a,b) {return a-b}
}
//这里输出的结果在不同的浏览器就会可能不一样了,大多数浏览器会忽略 ifTrue 的值直接的返回第二个声明,Firefox 会在ifTrue 为 true的时候返回第一个。这里我们使用函数表达式就能正确的输出结果了。
四、 构造函数和普通函数又什么区别?
主要是和普通函数在功能上的区别,我们知道函数就是一段能执行重复工作的代码。而这里根据他们的功能不同分为了构造函数和普通函数。
往往我们使用构造函数来进行初始化对象,而使用构造函数还往往会和 new 一起使用 ,并且一般会在定义的时候首字母大写。根据这几个特点我们也能耐比较好的区分。
面试回答🙋♂️🙋♂️🙋♂️
有些时候面试会问到 箭头函数 和普通函数的的区别。那么我们怎么回答呢?
箭头函数是在 ES6 中出现的新的写法,能够更加简洁,方便的书写定义。往往我们在一些场景,比如回调函数中要保存外部的 this ,使用箭头函数就不需要了。
在使用箭头函数时,我们不需要使用关键词 function ,也不需要命名,同时箭头函数还有一些特性我们需要留意,那就是箭头函数本身没有 prototype 属性 ,所以也就没有 this,在箭头函数中调用 this 的话,会指向其所在的上下文,并且使用 call 等方法也没办法修改,同时也不能使用 new 来创建构造函数。另外在箭头函数中没有 arguments对 象,只能使用剩余参数来获取。
上面我们说道在箭头函数中不能使用 argument ,要使用剩余参数来获取,那么这两种有什么区别呢?
一、剩余参数
如果函数的最后一个命名参数以…为前缀,则它将成为一个由剩余参数组成的真数组。使得我们可以将一个不定数量的参数表示为一个数组。
//例子如下:
function fun(a, b, ...theArgs) {
//...
}
function add(...theArgs) {
return theArgs.reduce(
(previousValue, currentValue) => previousValue + currentValue,
);
}
console.log(add(1,2,3)); //6
console.log(add(1,2,3,4)); //10
二、argument
arguments 是一个对应于传递给函数的参数的类数组对象。
function func(a, b, c) {
console.log(arguments[0]); //1
console.log(arguments[1]); //2
console.log(arguments[2]); //3
}
func(1, 2, 3);
三、剩余参数 和 argument 的区别
上面简单说了一下两者使用,其实最主要还是了解两者的区别!
注意❗❗❗
问:什么是类数组?
答:千万不要搞错了,argument 不是一个数组,尽管我们使用的时候是通过 argument[index] 来获取的!其实类数组是一个对象,并且有两个特点:
它的 key 是数字,或者数字的字符串形式拥有 length属性但因为其实不是数组,所以不支持数组的方法,另外通常类数组可能还拥有一些别的属性。
另外我们可以使用下面两种方法来变类数组为数组
- Array.prototype.slice.call(arrLike)
- Array.from(arrLike)
不得不说,解构赋值真的好用,但是今天,万能的解构赋值居然报错了,看了眼看出问题是啥,但是居然不知道扎解决,过去这种问题都是用可选连和判断空值的,这好像不能和我高大上的解构赋值一起搞呀,问了问大佬,妙及了,马上给我丢了几个案例,这里分享一波~~
基本的结构赋值我就不细说了,大家随便都能查到,这里主要想说的是多层嵌套对象该如何解构
我们看一个例子

我们需要解构出第三个箭头的数组出来,我们可以下面的写法(为什么 _statistic 是中括号的的实际关系到一个属性访问和键访问的知识点,区别在于是否符合标识符的命名规范~)

上面的这种写法是可以读取出来我们解构的值的,但是会报错

类似这个错,说读取不到project
那么这是为什么呢?这是因为我们的是对象嵌套对象,的那个我们遇到对象嵌套对象的时候就要小心了,经常会出现各种的问题,往往会有:读取到值,功能正常,但是会报错,或者值传递进去了但我们对其赋值操作不成功。(这个可以用监听并且加上 immediate: true,和 deep: true)
只是因为当我们一个值为null的时候,他也是对象,所以在一开始这个值还没有的时候是为null的,那么也就没有他嵌套的属性,所以会报cannot read
这时候往往要做的是空值判断,我们可以使用可选链或者判断空值来解决
空值判断

可选链
//比如上图那个:通过加问号就好了
data.owner?.avatar
回到主题,那么我们上面解构报错的问题就知道了~就是出现空值的情况,那么如何解决呢?
那就是设置缺省值 ,设置缺省值为{}

开头的那段代码,改为这样就不会报错了~
模板字面量是在 ES2015 后出现的,他解决恶魔问题呢?
模板字面量 是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。
具体我们看下面的例子:
let obj = {
name = "aaa";
age = 15
}
console.log("我的名字叫" + obj.name + ",今年已经" + obj.age + "岁啦!" )
我们卡以看到,当我们需要输出,创建字符串的时候,非常的麻烦!而我们要是使用模板字符串:
let obj = {
name = "aaa";
age = 15
}
console.log(`我的名字叫${obj.name},今年已经${obj.age}岁啦!`)
是不是简便了很多,使用模板字符串的格式:
用一对 ` 包裹,然后在需要插入变量的地方,吧变量放到 ${} 里面就可以了。
symbol 是 ES6中新添加的基本数据类型。它的作用是定义一个唯一的对象属性名,也就是唯一的标签,且不可变。
一、为什么需要 symbol 呢?
symbol 的出现之一是解决全局变量名冲突的问题。具体是怎样呢?
现在我们往往是多人协作,模块化的开发,如果有一天,你需要修改一个对象,而需要给这个对象添加一个属性 aaa ,就会出现如果你没有好好读之前的代码,而在其他的 js 文件也添加属性 aaa ,你就可能出现添加了一个同名的属性 aaa。就出现冲突,覆盖了。
那么如何使用呢?
//可以传入参数,参数书对 symbol 的描述,可以调用,但是不能访问 symbol 本身
var sym1 = Symbol();
var sym2 = Symbol('foo');
var sym3 = Symbol('foo');
console.log(typeof sym1); // symbol
console.log(Symbol("foo") === Symbol("foo")); // false
console.log(sym2 == sym3 );//false
//Symbol 包装器对象作为属性的键
var name = Symbol("名字");
var obj = {};
obj[name] = "大黄";
// 作为对象属性 key 时,在获取其值的时候,只能使用 [] 形式获取,不能使用 . 操作符
console.log(obj);//{Symbol(name): "大黄"}
console.log(obj[name]);//大黄
console.log(obj.name);//undefined
//不能被 for in
var name = Symbol("名字");
var obj = {};
obj[name] = "大黄";
for(var key in obj){
console.log(obj[key]);//无输出
}
// 用作唯一常量,标识符等
var animal= {
rabbit: Symbol(),
dog: Symbol(),
snake: Symbol()
}
使用场景
作为属性名,避免属性名冲突定义私有,为对象定义一些非私有的、但又希望只用于内部的方法作为唯一值,在一些场景,我们是不在意值是什么的,那么我们可以使用 symbol 来表示特点
不能使用 new 来构建,原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 除了如 new Boolean、new String以及new Number,因为遗留原因仍可被创建。不能使用点操作符,作为对象属性 key 时,在获取其值的时候,只能使用 [] 形式获取,不能使用 . 操作符不能被 for in迭代, 也不能用 Object.getOwnPropertyNames() 返回对象的属性,只能使用 Object.getOwnPropertySymbols() 得到它们。总而言之: symbol 的作用就是生成一个唯一值,全局唯一的
一、 重排(也叫回流)
重排就是浏览器在第一次渲染完页面布局以后,后续引起页面各节点位置重新计算或者重新布局的行为二、重绘
重绘就是布局计算完毕后,页面会重新绘制,这时浏览器会遍历渲染树,绘制每个节点,当元素外观变化但没有改变布局的时候,重新把元素绘制的过程
重绘不一定出发重排,但重排一定会出发重绘
如:vidibility、outline、背景色等属性的改变
举个生动的例子来说就是,我们可以理解重排为一个人的身体,而重绘为一个人的外观,显而易见,当你长胖或者长高了,都会引起身体的变化
但是比如你化个妆,涂个口红啥的,就只是改变你的外表,是重绘,不会说因此你的身体就改变了。
我们常常说HTML是网页的结构,CSS是网页的外观,JS是网页的动作,那么一般涉及到网页的HTMl改变的(也即是DOM元素改变)的就是重排,而涉及到CSS的比如改变颜色等就是重绘(对于会影响到DOM的不算,比如使用了display:flex)
三、 如何减少或避免
面试回答🙋♂️🙋♂️🙋♂️
关于重绘和重排,我举个例子比如我们一个人,我们出生就有手 / 脚 / 脑袋 一个完整的结构,一般情况下我们的结构是不会变的,不会突然多一只手和脚,但是我们可以通过化妆或穿上不同的衣服来改变我们的形象。而重绘和重排就是类似这样的关系。当发生影响页面布局的操作,比如改变大小,位置之类的,就会触发重排,而当改变背景颜色这一类就是重绘。
一、绝对定位和相对定位
absolute:定位是相对于离元素最近的设置了绝对或相对定位的父元素决定的,如果没有父元素设置绝对或相对定位,则元素相对于根元素即 HTML 元素定位。设置了absolute的元素脱了了文档流,元素在没有设置宽度的情况下,宽度由元素里面的内容决定。脱离后原来的位置相当于是空的,下面的元素会来占据位置。

relative:定位是相对于自身位置定位(设置偏移量的时候,会相对于自身所在的位置偏移)。设置了 relative 的元素仍然处在文档流中,元素的宽高不变,设置偏移量也不会影响其他元素的位置。最外层容器设置为 relative 定位,在没有设置宽度的情况下,宽度是整个浏览器的宽度。

二、position 属性
既然说道了相对定位和绝对定位,他们用到的都是 position 属性,那么我们来看看该属性的其他属性值吧!
static
该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top, right, bottom, left 和 z-index 属性无效。
relative(相对定位)
该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。position:relative 对 table-*-group, table-row, table-column, table-cell, table-caption 元素无效。
absolute(绝对定位)
元素会被移出正常文档流,并不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先元素的偏移,来确定元素位置。绝对定位的元素可以设置外边距(margins),且不会与其他边距合并。
fixed(固定定位)
元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed 属性会创建新的层叠上下文。当元素祖先的 transform, perspective 或 filter 属性非 none 时,容器由视口改为该祖先。
sticky(粘性定位)
元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor)和 containing block (最近块级祖先 nearest block-level ancestor),包括table-related元素,基于top, right, bottom, 和 left的值进行偏移。偏移值不会影响任何其他元素的位置。
该值总是创建一个新的层叠上下文(stacking context)。注意,一个sticky元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的overflow 是 hidden, scroll, auto, 或 overlay时),即便这个祖先不是最近的真实可滚动祖先。这有效地抑制了任何“sticky”行为(详情见Github issue on W3C CSSWG)
问:想对子元素启用绝对定位来的,但没有生效?
解答:是因为没有注意到绝对定位的使用条件:
- 绝对定位使用通常是父级定义position:relative定位
- 子级定义position:absolute绝对定位属性
- 并且子级使用left或right和top或bottom进行绝对定位。
当时没有给父元素设置 position:relative 导致没有生效。
推荐文章:position
关于浮动我们往往在开始接触前端,写 CSS 的时候就会了解过,这里就简单和大伙们回顾一下吧。其实吧 flost 用的很少了,一般用框架,使用 flex了 ~~
简单来说:当一个元素浮动之后,它会被移出正常的文档流,然后向左或者向右平移,一直平移直到碰到了所处的容器的边框,或者碰到另外一个浮动的元素。
一、浮动的特点:
那么浮动有什么特点:
脱离文档流,不会影响普通元素的布局

内联排序,也就是多个浮动的元素,会类似行内元素,如下图的 1 和 2 这两个div就是同时对他们设置了 ,向左浮动

二、浮动的使用场景
而起初提出浮动的概念主要是为了解决左边图片,右边文字的需求,现在往往在我们需要实现一些布局的时候,比如多个盒子一行显示等场景
三、为什么要清除浮动:
那就是浮动会导致的一个问题,也就是高度塌陷:
什么是高度塌陷呢?其实就是指 当父元素没有给确定的高度(父元素高度靠子元素撑起来),而当子元素添加了浮动时,内容无法撑起父元素的高度,即父元素发生高度塌陷。
为什么会发生高度塌陷呢?我们知道当元素浮动的时候,其实这个元素已经脱离了文档流了,也就无法支撑起父元素了。
那么如何解决呢?
推荐文章: 最详细的高度塌陷解决方案
一、什么是 BFC
BFC是网页的一块区域,里面的元素都基于这块区域布局。虽然BFC本身是环绕文档流的一部分,但它将内部的内容与外部的上下文隔离开。这种隔离为创建BFC的元素做出了以下3件事情。
简而言之,BFC里的内容不会跟外部的元素重叠或者相互影响。如果给元素增加clear属性,它只会清除自身所在BFC内的浮动。如果强制给一个元素生成一个新的BFC,它不会跟其他BFC重叠。
二、如何创建 BFC
给元素添加以下的任意属性值都会创建BFC。
备注:网页的根元素也创建了一个顶级的BFC
为什么出现
css 引入伪类和伪元素概念是为了格式化文档树以外的信息。也就是说,伪类和伪元素是用来修饰不在文档树中的部分。
一、伪类
用于定义元素的特殊状态。
常见的有:
:focus / :hover / :empty / :active
二、伪元素
用于设置元素指定部分的样式。比如我们需要对一段文字的第一行设置颜色为红,但是其他部分为黑就能用到 ::first-line(其实这里会创建一个span把这一行包起来,但是我们在文档流中又是看不到的)
常见的有:
::after / ::before / ::selection / ::first-line / ::first-letter
三、伪元素和伪类的区别
伪类是操作文档中已有的元素,而伪元素是创建了一个文档外的元素,两者最关键的区别就是这点。一般伪类是单冒号,如:hover,而伪元素是双冒号::before
这里拷贝过来便于查阅
| 选择器 | 例子 | 例子描述 |
|---|---|---|
| .class | .intro | 选择 class=“intro” 的所有元素。 |
| .class1.class2 | .name1.name2 | 选择 class 属性中同时有 name1 和 name2 的所有元素。 |
| .class1 .class2 | .name1 .name2 | 选择作为类名 name1 元素后代的所有类名 name2 元素。 |
| #id | #firstname | 选择 id=“firstname” 的元素。 |
| * | * | 选择所有元素。 |
| element | p | 选择所有 元素。 |
| element.class | p.intro | 选择 class=“intro” 的所有 元素。 |
| element,element | div, p | 选择所有
元素和所有
元素。 |
| element element | div p | 选择
元素内的所有
元素。 |
| element>element | div > p | 选择父元素是
的所有
元素。 |
| element+element | div + p | 选择紧跟
元素的首个
元素。 |
| element1~element2 | p ~ ul | 选择前面有 元素的每个
|
| [attribute] | [target] | 选择带有 target 属性的所有元素。 |
| [attribute=value] | [target=_blank] | 选择带有 target=“_blank” 属性的所有元素。 |
| [attribute~=value] | [title~=flower] | 选择 title 属性包含单词 “flower” 的所有元素。 |
| [attribute | =value] | [lang |
| [attribute^=value] | a[href^=“https”] | 选择其 src 属性值以 “https” 开头的每个 元素。 |
| [attribute$=value] | a[href$=“.pdf”] | 选择其 src 属性以 “.pdf” 结尾的所有 元素。 |
| [attribute*=value] | a[href*=“w3schools”] | 选择其 href 属性值中包含 “abc” 子串的每个 元素。 |
| :active | a:active | 选择活动链接。 |
| ::after | p::after | 在每个 的内容之后插入内容。 |
| ::before | p::before | 在每个 的内容之前插入内容。 |
| :checked | input:checked | 选择每个被选中的 元素。 |
| :default | input:default | 选择默认的 元素。 |
| :disabled | input:disabled | 选择每个被禁用的 元素。 |
| :empty | p:empty | 选择没有子元素的每个 元素(包括文本节点)。 |
| :enabled | input:enabled | 选择每个启用的 元素。 |
| :first-child | p:first-child | 选择属于父元素的第一个子元素的每个 元素。 |
| ::first-letter | p::first-letter | 选择每个 元素的首字母。 |
| ::first-line | p::first-line | 选择每个 元素的首行。 |
| :first-of-type | p:first-of-type | 选择属于其父元素的首个 元素的每个 元素。 |
| :focus | input:focus | 选择获得焦点的 input 元素。 |
| :fullscreen | :fullscreen | 选择处于全屏模式的元素。 |
| :hover | a:hover | 选择鼠标指针位于其上的链接。 |
| :in-range | input:in-range | 选择其值在指定范围内的 input 元素。 |
| :indeterminate | input:indeterminate | 选择处于不确定状态的 input 元素。 |
| :invalid | input:invalid | 选择具有无效值的所有 input 元素。 |
| :lang(language) | p:lang(it) | 选择 lang 属性等于 “it”(意大利)的每个 元素。 |
| :last-child | p:last-child | 选择属于其父元素最后一个子元素每个 元素。 |
| :last-of-type | p:last-of-type | 选择属于其父元素的最后 元素的每个 元素。 |
| :link | a:link | 选择所有未访问过的链接。 |
| :not(selector) | :not§ | 选择非 元素的每个元素。 |
| :nth-child(n) | p:nth-child(2) | 选择属于其父元素的第二个子元素的每个 元素。 |
| :nth-last-child(n) | p:nth-last-child(2) | 同上,从最后一个子元素开始计数。 |
| :nth-of-type(n) | p:nth-of-type(2) | 选择属于其父元素第二个 元素的每个 元素。 |
| :nth-last-of-type(n) | p:nth-last-of-type(2) | 同上,但是从最后一个子元素开始计数。 |
| :only-of-type | p:only-of-type | 选择属于其父元素唯一的 元素的每个 元素。 |
| :only-child | p:only-child | 选择属于其父元素的唯一子元素的每个 元素。 |
| :optional | input:optional | 选择不带 “required” 属性的 input 元素。 |
| :out-of-range | input:out-of-range | 选择值超出指定范围的 input 元素。 |
| ::placeholder | input::placeholder | 选择已规定 “placeholder” 属性的 input 元素。 |
| :read-only | input:read-only | 选择已规定 “readonly” 属性的 input 元素。 |
| :read-write | input:read-write | 选择未规定 “readonly” 属性的 input 元素。 |
| :required | input:required | 选择已规定 “required” 属性的 input 元素。 |
| :root | :root | 选择文档的根元素。 |
| ::selection | ::selection | 选择用户已选取的元素部分。 |
| :target | #news:target | 选择当前活动的 #news 元素。 |
| :valid | input:valid | 选择带有有效值的所有 input 元素。 |
| :visited | a:visited | 选择所有已访问的链接。 |
一 、首先我们看看我们 CSS 有多少种样式类型:
二 、常见选择器及选择器权重
| 选择器 | 格式 | 优先级权重 |
|---|---|---|
| id选择器 | #id | 100 |
| 类选择器 | .classname | 10 |
| 属性选择器 | a[ref = “eee”] | 10 |
| 伪类选择器 | li:last-child | 10 |
| 标签选择器 | div | 1 |
| 为元素选择器 | li:after | 1 |
| 相邻兄弟选择器 | h1 + p | 0 |
| 子选择器 | ul > li | 0 |
| 后代选择器 | li a | 0 |
| 通配符选择器 | * | 0 |
三 、注意事项
这两个属性都是让元素隐藏,不可见。两者的区别主要分两点
1 、是否在渲染树中
2 、是否是继承属性
这两者的关系类似于 v-if 和 v-show 之间的关系
我们常见的隐藏属性的方法有 display: none 与 visibility: hidden,上面我们也分析过这两者的区别,下面我们看看还有的比的一些隐藏元素的方式:
在VUE中还有 v-if 和 v-show 这两个指令,在目录你能看到关于这两个的使用区别
补充:不过实际企业开发中好像使用BEM的也比较多,因为这样更加便于阅读和可维护
这几个单位是我们在写长度的时候常常会用到的,那么他们分别对应什么场景呢,有什么特点呢?下面我们来看看:
一、px
px:也就是像素,是基于屏幕分辨率来说的,一旦设置了,就无法适应页面大小的变化。
二、em
em:是相对单位,相对于当前对象内文本的字体大小(也就是它的父元素),如果当前对象内文本的字体没有设置大小,就会相对于浏览器默认字体大小也就是16px。所以在没有设置的情况下 1em = 16px。
为了便于运算你可以在body选择器中声明Font-size=62.5%;这就使em值变为 16px*62.5%=10px, 这样12px=1.2em, 10px=1em, 也就是说只需要将你的原来的px数值除以10,然后换上em作为单位就行了。
三、rem
rem:rem的出现是为了解决em的问题的,em是相对于父元素来说的,这就要求我们进行任何元素设置的时候,都需要知道它的父元素字体的大小。而 rem是相对于根元素,这样就意味着,我们只需要在根元素确定一个参考值,就可以了,同时还能做到只修改根元素就成比例地调整所有字体大小。
四、vh、vw
vh、vw:vw 是根据窗口的宽度。会把窗口的大小分为100份,所以50vw代表窗口大小的一半。并且这个值是相对的,当窗口大小发生改变也会跟着改变,同理,vh则为窗口的高度
四、总的来说,四者的区别:
px 是固定的大小,em 是相对于父元素字体的大小,rem 是相对于根元素字体的大小,vh、vw 是相对可是窗口的大小
五、使用场景的区别:
我们知道在谷歌浏览器中,字体的最小值为 12px,当你设置字体为 10px 的时候,结果显示的还是12px,这是因为 Chrome 浏览器做了如下限制:
那么设计给你的小于12px,该如何处理呢?
提刀找设计,简简单单,告诉他(男设计)低于12是不可的~~
要是设计是个妹子呢?那么我们肯定是有求必应的,怎么能说不行呢?
首先我们的 CSS3 有个新的属性是:transform:scale(0.8)
-webkit-transform-origin-x: 0; /*缩小后文字居左*/
-webkit-transform: scale(0.80); /*关键*/
transform:scale(0.8);
注意点:
transform:scale()这个属性只可以缩放可以定义宽高的元素,而行内元素是没有宽高的,我们可以加上一个display:inline-block;
麻了,每次都是要用的时候才去查,这里整理总结一下,可以实现div居中的几种方式
方式一
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
方式二
可以给父元素添加下面的属性,利用 flex 布局来实现
display: flex;
align-items: center;
flex-direction: column
方式三
通过定位和变形来实现
给父元素添加 position: relative;相对定位。
给自身元素添加position: absolute;绝对定位。
top: 50%;使自身元素距离上方“父元素的50%高度”的高度。
left: 50%;使自身元素距离上方“父元素的50%宽度”的宽度。
transform: translate(-50%,-50%);使自身元素再往左,往上平移自身元素的50%宽度和高度。
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
方式四
这个是实现内容文本居中的,坑死了,之前没留意在一个全局的文件加了,后面很多组件里面的内容都居中了,还一时没发现,虽然想到会不会是全局文件的问题,但一下子眼拙没看到,结果捣鼓半天
body{ text-align:center}
src ⽤于替换当前元素,href ⽤于在当前⽂档和引⽤资源之间确⽴联系
一、src
src 是 source 的缩写,指向外部资源的位置,指向的内容将会嵌⼊到⽂档中当前标签所在位置;在请求 src 资源时会将其指向的资源下载并应⽤到⽂档内,例如 js 脚本,img 图⽚和 frame 等元素。
当浏览器解析到该元素时,浏览器会对这个文件进行解析,编译和执行,从而导致整个页面加载会被暂停,类似于将所指向资源嵌⼊当前标签内。这也是为什么将js 脚本放在底部⽽不是头部。
二、href
href 是 Hypertext Reference 的缩写,指向⽹络资源所在位置,建⽴和当前元素(锚点)或当前⽂档(链接)之间的链接,浏览器遇到 href 就会并⾏下载资源并且不会停⽌对当前⽂档的处理。 这也是为什么建议使⽤ link ⽅式来加载 css,⽽不是使⽤@import 。
三、总结来说
src 代表这个资源是必备的,必不可少的,最终会嵌入到页面中,而 href 是资源的链接
一、语法结构
//link
<link href="路径" rel="stylesheet" type="text/css" />
//import
@import url(路径地址);
二、区别
1、从属关系区别
@import是 CSS 提供的语法规则,只有导入样式表的作用;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。
2、加载顺序区别
加载页面时,link标签引入会和 html 同时加载;而 @import 引入的 CSS 将在页面加载完毕后被加载。
3、兼容性区别
@import是 CSS2.1 才有的语法,可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。
4、DOM可控性区别
可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。
5、权重
link方式的样式权重高于@import的权重。
总的来说,使用 link 会比 @import 好,首先兼容性, link 作为 html 的标签是不会存在这个问题的。并且如果使用 @import 是在 dom 树渲染完才会进行渲染的,所以是不被 JavaScript 动态的修改的
1. 利用 border
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div class="border">div>
body>
<style type="text/css">
.border{
width: 100px;
height: 100px;
border-color: red orange yellow green;
border-width: 50px;
border-style: solid;
}
style>
html>
我们可以看出,其实我们的每一条边,其实是一个梯形。
而当你把内容的宽高都设置为 0 的时候,就会发现出现了我们想要的三角形了

那么我们这时候只需要把不需要的三角形的颜色设置为透明,就能剩下我们想要的了。但还有个问题,虽然设置为透明,但是实际上还是占据了高度呢(进去F12我们可以查看到),这里我们再设置border-bottom-width: 0 就能实现我们想要的结果了。

最终效果:
<style type="text/css">
.border{
width: 0px;
height: 0px;
border-color: red transparent transparent transparent;
border-width: 50px;
border-style: solid;
border-bottom-width: 0
}
</style>

2. 使用svg 来实现
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<polygon points="220,100 300,210 170,250"
style="fill:#cccccc;
stroke:#000000;stroke-width:1"/>
</svg>
3. 贴图
简单粗暴,叫 UI 小姐姐用 AI 画一个给你,多好呀,增加和小姐姐互动的机会。嗯,要是你已经有小姐姐的话,自己上 阿里的 IconFont 上面下载一个吧~
4. 旋转矩形
使用 transform: rotate 和 overflow: hidden 也是能实现的

.triangle {
width: 141px;
height: 100px;
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: deeppink;
transform-origin: left bottom;
transform: rotate(45deg);
}
}
5. Canvas 实现
其实实现的方法还有很多,比如使用字符绘制,使用clip-path,还有使用渐变,这里就简单介绍几种
针对这个问题只要我们有两点:
消除浏览器之间的差异,提高兼容性提高代码质量一、首先我们来回答第一点:
我们有些时候没有初始化的时候,是不是会发现当我们添加进去一个 DIV的时候,会发现他并不是紧贴着窗口的,而是有一定的距离的。这就是一个例子,在不同的浏览器中,对一些标签是具有的默认值的,而且不同的浏览器默认值也肯是不一样的。如果没有对其 CSS样式做初始化,就可能导致在不同的浏览器之间展示的效果是不一样的。
二、关于第二点:
这点也是初始化后,便于我们对代码的统一管理,减少重复样式等。同时修改的时候便于统一管理。
我个人喜欢用这套来初始化CSS样式,供参考:
body,ol,ul,h1,h2,h3,h4,h5,h6,p,th,td,dl,dd,form,fieldset,legend,input,textarea,select{margin:0;padding:0}
body{font:12px"宋体","Arial Narrow",HELVETICA;background:#fff;-webkit-text-size-adjust:100%;}
a{color:#2d374b;text-decoration:none}
a:hover{color:#cd0200;text-decoration:underline}
em{font-style:normal}
li{list-style:none}
img{border:0;vertical-align:middle}
table{border-collapse:collapse;border-spacing:0}
p{word-wrap:break-word}
简单粗暴: {padding: 0; margin: 0;}
有些时候回我们会这样来写,但是这样写有一个弊端,就是在网站比较大时,CSS样式文件也很大,这样写时,它会把所有的标签都初始化一遍,这样会加大网站运行的负荷,会让网站加载的时候需要很长一段时间。
继承属性:
非继承属性:
那么这两类属性都有哪些呢?
一、无继承性的属性
1、display:
2、文本属性:
3、盒子模型的属性:
4、背景属性:
5、定位属性:
6、生成内容属性:
7、轮廓样式属性:
8、页面样式属性:
9、声音样式属性:
二、有继承性的属性
1、字体系列属性
2、文本系列属性
3、元素可见性:
4、表格布局属性:
5、列表布局属性:
6、生成内容属性:
7、光标属性:
8、页面样式属性:
9、声音样式属性:
三、所有元素可以继承的属性
1、元素可见性:
2、光标属性:
四、内联元素可以继承的属性
1、字体系列属性
2、除text-indent、text-align之外的文本系列属性
五、块级元素可以继承的属性
1、text-indent、text-align
这个有些时候我们会没有注意到,往往我们会使用 px 等单位来,但是当我们使用百分比的时候回呢,今天我们来探讨一下。
总结来说:当 padding 属性值为百分比的时候,如果父元素有宽度,相对于父元素宽度,如果没有,找其父辈元素的宽度,均没设宽度时,相对于屏幕的宽度。
不管 margin-top/margin-bottom 还是 margin-left/margin-right(对于padding 同样)也是,参考的都是 width。
那么为什么是 width, 而不是 height 呢?
CSS权威指南中的解释:
我们认为,正常流中的大多数元素都会足够高以包含其后代元素(包括外边距),如果一个元素的上下外边距时父元素的 height 的百分数,就可能导致一个无限循环,父元素的 height 会增加,以适应后代元素上下外边距的增加,而相应的,上下外边距因为父元素 height 的增加也会增加,如此循环。
margin 合并也叫外边框塌陷或者外边距重叠,注意不同于高度塌陷噢。
外边距重叠:块的上外边距(margin-top)和下外边距(margin-bottom)有时合并(折叠)为单个边距,其大小为单个边距的最大值(或如果它们相等,则仅为其中一个),这种行为称为边距折叠。
那么什么情况会造成呢?
1、同一层相邻元素之间
<p>1 </p>
<p>2 </p>
<style>
p:nth-child(1){
margin-bottom: 13px;
}
p:nth-child(2){
margin-top: 12px;
}
</style>
这里我们希望的是这两个之间的距离是 25px,但是实际上他们的距离是13px
2、没有内容将父元素和后代元素分开
<style type="text/css">
section {
margin-top: 13px;
margin-bottom: 87px;
}
header {
margin-top: 87px;
}
footer {
margin-bottom: 13px;
}
</style>
<section>
<header>上边界重叠 87</header>
<main></main>
<footer>下边界重叠 87 不能再高了</footer>
</section>
3、空的块级元素
<style>
p {
margin: 0;
}
div {
margin-top: 13px;
margin-bottom: 87px;
}
</style>
<p>上边界范围是 87 ...</p>
<div></div>
<p>... 上边界范围是 87</p>
一、元素种类
1、行内元素
<a >
<strong>
<b>
<em>
<del>
<span >
<img>
<input>
<select>
2、块级元素
<h1>~<h6>
<p>
<div>
<ul>
<ol>
<li>
<div>
<dl>
3、空元素
<br> <hr> <img> <input> <link> <meta>
二、行内元素和块级元素的转换
//定义元素为块级元素
display:block
//定义元素为行内元素
display : inline
//定义元素为行
display:inline-block
三、块级元素和行内元素的区别
我们区分 块级元素 和 行内元素,首先行内元素是在一行中能有多个的,块级元素是自己占一行的。
接着可以从三个方面来查看
面试回答🙋♂️🙋♂️🙋♂️
行内元素和块级元素在面试中问的倒是不多,但是在笔试中出现的频率就很高了~
区分行内元素和块级元素很好理解,顾名思义,行内就是能都在一行里的,那么我们作为前端开发有哪些呢?你想想是不是 一行可以有多个 < a > 标签 ,是不是能有多个 < input > 标签,而同类的还有 < span > / < img > / < strong > 等,这不就很容易记住了。而块级元素就是要自己占一行的,那不就有 < div > ,还有我们使用的列表 < ul > / < ol >,除此之外还有 < h1 >~< h6 > / < p > 等等。
在前端中我们实现动画的方式有两种 ,也就是 JavaScript动画 和 Css动画。
一。JavaScript 动画
一般 我们使用 JavaScript来实现一些比较复杂的动画,或者 CSS动画,没办法实现的效果,那么如何实现一个 JavaScript动画呢?JavaScript动画的原理是什么呢?
简单来说:JavaScript 动画就是通过对元素样式进行渐进式变化编程完成的。这种变化通过一个计数器来调用。当计数器间隔很小时,动画看上去就是连贯的。 实践中一般需要设置 style = “position: relative” 创建容器元素。通过 style = “position: absolute” 创建动画元素。然后通过定时器(绘图函数)来控制动画元素的变化,也就是按照我们的规则来修改 Css样式。
二、Css 动画
与 JavaScript动画使用定时器修改不同,Css动画是通过指定 @keyframes 来指定动画效果,然后绑定到需要实习的元素上。
一般有属性:
@keyframes:规则中指定了 CSS 样式,动画将在特定时间逐渐从当前样式更改为新样式。
animation-name:用来绑定动画规则
animation-duration:定义需要多长时间才能完成动画
animation-delay:规定动画开始的延迟时间。也就是多久后才开始动画
animation-iteration-count:指定动画应运行的次数。
animation-direction:指定是向前播放、向后播放还是交替播放动画。
animation-timing-function:规定动画的速度曲线。
animation-fill-mode:SS 动画不会在第一个关键帧播放之前或在最后一个关键帧播放之后影响元素。animation-fill-mode 属性能够覆盖这种行为。
animation:实现与上例相同的动画效果,也就是类似我们的 font 可以包含所有的相关属性
//下面的例子将 "example" 动画绑定到 元素。动画将持续 4 秒钟,同时将 元素的背景颜色从 "red" 逐渐改为 "yellow":
/* 动画代码 */
@keyframes example {
from {background-color: red;}
to {background-color: yellow;}
}
/* 向此元素应用动画效果 */
div {
width: 100px;
height: 100px;
background-color: red;
animation-name: example;
animation-duration: 4s;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
三、JavaScript 动画 和 Css动画优缺点
JS动画
优点:
-
过程可控,在动画中可以实现任何效果,暂停,返回,加速等等
-
动画效果丰富,可以根据绘图函数实现任意效果,跳跳球,变速等
-
兼容性好,使用 CSS3 存在兼容问题,但是 JavaScript 几乎没有
缺点:
-
JavaScript 在浏览器的主线程中运行,而主线程中还有其它需要运行的JavaScript脚本、样式计算、布局、绘制任务等,对其干扰导致线程可能出现阻塞,从而造成丢帧的情况。
-
代码的复杂度高于CSS动画。
CSS动画
优点(浏览器可以对动画进行优化):
-
集中所有DOM,一次重绘重排,刷新频率和浏览器刷新频率相同。
-
代码简单,方便调试
-
不可见元素不参与重排,节约CPU
-
可以使用硬件加速(通过 GPU 来提高动画性能)。
缺点:
- 运行过程控制较弱,无法附加事件绑定回调函数。
- 代码冗长。
二十三、知识点:说说你对 SCSS 的理解 ⭐⭐⭐
在开发中, SCSS 也是很常用的该工具了,面试的时候,面试官会为了考察你的能力,往往会问一下,看下你的掌握,特别是你在简历中写道过你会 SCSS 的,下面我们来列举一下一些常见的属性。
1、 嵌套
$baseColor: red;
$color1: green;
body{
background-color: $baseColor;
$color2: yellow;
div{
background-color:$color2;
color: $color1;
}
div2{
background-color:$color2;
color: $color1;
}
}
//编译后
body {
background-color: red;
}
body div {
background-color: yellow;
color: green;
}
body div2 {
background-color: yellow;
color: green;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
2、 定义变量
$baseColor: red;
$color1: green;
body{
background-color: $baseColor;
}
div{
background-color:$baseColor;
color: $color1;
}
//编译出的结果
body {
background-color: red; }
div {
background-color: red;
color: green; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
3、 混合
@mixin box-sizing($sizing) {
-webkit-box-sizing: $sizing;
-moz-box-sizing: $sizing;
box-sizing: $sizing;
}
.box-border{
border: 1px solid red;
@include box-sizing(border-box)
}
//编译结果
.box-border {
border: 1px solid red;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
4、 继承
.A{
color: red;
border: #0D47A1 2px solid;
background-color: #0e0e0e;
}
.B{
@extend .A;
}
.C{
@extend .A;
border-color: #00a507;
}
//编译结果
.A, .B, .C {
color: red;
border: #0D47A1 2px solid;
background-color: #0e0e0e;
}
.C {
border-color: #00a507;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
还有很多种用法,我们这里不全部展示了,可以看下面的文章,同时扩展对SCSS的学习
推荐文章:
二十四、知识点:说说 CSS3 有什么新特性 ⭐⭐⭐
我们会使用到很多CSS属性,但是你了解 CSS3 中具体有哪些新特性?(话说,很多东西日常使用,但是不总结就会其实很多了解不够全面,或者有所缺失,也不便于我们学习的体系化)
下面我们来看看有哪些吧!
-
新增选择器: p:nth-child(n){color: rgba(255, 0, 0, 0.75)}
-
弹性盒模型:
弹性布局: display: flex;
栅格布局: display: grid;
-
渐变
线性渐变: linear-gradient(red, green, blue);
径向渐变:radial-gradient(red, green, blue)
-
边框
border-radius:创建圆角边框
box-shadow:为元素添加阴影
border-image:使用图片来绘制边框
-
阴影
box-shadow:3px 3px 3px rgba(0, 64, 128, 0.3);
-
背景
用于确定背景画区:background-clip
设置背景图片对齐:background-origin
调整背景图片的大小background-size
控制背景怎样在这些不同的盒子中显示:background-break
多列布局: column-count: 5;
-
text-overflow
文字溢出时修剪:text-overflow:clip
文字溢出时省略符号来代表:text-overflow:ellipsis
-
transform 转换
transform: translate(120px, 50%):位移
transform: scale(2, 0.5):缩放
transform: rotate(0.5turn):旋转
transform: skew(30deg, 20deg):倾斜
-
animation 动画
animation-name:动画名称
animation-duration:动画持续时间
animation-timing-function:动画时间函数
animation-delay:动画延迟时间
animation-iteration-count:动画执行次数,可以设置为一个整数,也可以设置为infinite,意思是无限循环
animation-direction:动画执行方向
animation-paly-state:动画播放状态
animation-fill-mode:动画填充模式
这里不全部列举了,还有多列布局、媒体查询(@media)、混合模式等等,而CSS3如今有很多是我们日常使用到的,比如我们在处理文字溢出时会使用 text-overflow ,比如我们需要文字突破限制12像素就可以使用 transform: scale(0.5)来调整。
计算机网络
一、知识点:Cookie ⭐⭐⭐
一、因为什么而诞生的:
首先 HTTP 是无状态的,那么就没办法记录用户的状态 ,比如用户是否已经登录了。举个例子我们登录淘宝网页后,当我们打开商品的详细页,我们也还是登录的,而不会是变为未登录。这里就用到了 Cookie。
二、如何实现,解决了什么:
当我们第一次向服务端请求时,服务端就会创建cookie,该cookie会包含用户信息等,并且返回给客户端。客户端存储在本地,当客户端再次访问该服务端时,就会把该cookie一起添加到http请求中,发送到服务器,就可以识别当前用户是否登录
三、同类的存在,区别
类似的浏览器存储还有session,localStorage,sessionStorage,token需要了解的。比较多,这里就不做深入了,但是是需要明白他们的区别的和使用场景的。
深化文章:浏览器存储中的存储 cookie, session, localStorage, sessionStorage
二、知识点:TCP 和 UDP 的区别 ⭐⭐⭐⭐⭐
一、主要区别
1.有无连接
- 假设现在有两台主机,它们是使用 UDP 协议进行通信,那么它们在发送数据之前,可以随机发送数据,而不需要进行连接,因此我们称 UDP 是无连接的(这里的连接是指逻辑连接关系)。
- 而如果两台主机是通过 TCP 协议进行通信的话,那么它们首先要通过“三次握手”进行连接,连接之后才可以发送数据,最后还需要使用“四次挥手”释放连接。
2.通信方式
- UDP 支持单播、多播和广播的方式
- 而 TCP 仅支持单播。这里涉及到了网络中的单播、多播和广播的知识,看下面这张图你就明白了。
- 多播只将用户数据报传输到网络中的部分主机,广播则将用户数据报传输到网络中的 全部 主机。
3.对应用层报文的处理
-
发送方应用进程将应用层报文交付给应用层 UDP,UDP 直接给应用层报文添加一个首部,使之成为应用层用户数据报,然后进行发送。接收方 UDP 接收到该报文以后,只需将首部去掉,然后将报文交付给接收方的应用进程就行了。UDP 不会对报文进行拆分,因此它是面向报文的。
-
TCP 则会将发送方的数据块仅仅看作一连串无结构的字节流,将它们编号并存储在缓存中,然后根据自己的发送策略,提取一定量的字节,加上首部构建成 TCP 报文段进行发送。最后接收方的 TCP 一方面将 TCP 报文中提取出数据并存储在缓存,另一方面将接收缓存中的一些字节交付给接收方的应用进程。
4.是否提供可靠传输服务
-
对于 UDP,发送方会一门心思给接收方不断地发送数据报,没有可靠性保证、顺序保证和流量控制字段等,可靠性较差。但是正因为UDP协议的控制选项较少,在数据传输过程中延迟小、数据传输效率高,适合对可靠性要求不高的应用程序如果发送过程中出现了误码、丢包的情况,接收方不会做任何处理,只管接收就行了。保证了实时性,所以网络直播、视频会议等使用 UDP 的传输方式。
-
TCP 收到报文准确无误后,会向发送方发送一个确认的报文,这样一来,如果收到了误码或者遇到丢包的情况,由于发送端没有收到确认消息,会进行超时重发,直到收到接收端的确认报文。下载文件、浏览网页时,我们希望数据没有出现丢失,因此它们使用 TCP 协议进行数据传输。
-
通过滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制
-
使用校验和,确认和重传机制来保证可靠传输
-
TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制
二、归纳总结
类别 UDP TCP 是否连接 无连接 面向连接 是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 连接对象个数 支持一对一,一对多,多对一,多对多交互通信 只能是一对一通信 传输方式 面向报文 面向字节流 首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节 适用场景 适用于实时应用,如视频会议、直播 适用于要求可靠传输的应用,如文件传输
深化文章:计算机网络-运输层(UDP/TCP协议)
三、知识点:什么是 HTTP ⭐⭐⭐⭐⭐
一、什么是 HTTP
HTTP 也就是超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。
二、HTTP/0.9
- 仅支持GET请求
- 不支持请求头
- 只能传输纯文本内容
- 无状态连接
三、HTTP/1.0
响应格式增加,响应的文本不再仅限于文本支持响应头和请求头默认短连接,每次建立连接后请求完成就断开,会导致连接无法复用,并且频繁的三次握手,但是增加了keep-alive关键字来由短链接变成长连接,就是请求报文里的字段指定Connection:keep-alive;新添请求方式,支持GET、POST、 HEAD请求。
短连接
四、HTTP/1.1
默认长连接,相比 HTTP/1.0 改为默认长链接 ,但可以 Connection:close来把长连接变成短连接;并且使用长连接,使得管道运输变为可能新增5种请求类型,OPTIONS,PUT, DELETE, TRACE, CONNECT增加了Host字段,在HTTP1.0中认为每台服务器都绑定一个唯一的ip地址,因此在URL中并没有传递主机名,但是随着虚拟机技术的发展,可能在一台物理机器上存在多个虚拟主机,并且他们共享了一个ip地址,HTTP1.1中请求消息和响应消息都支持 Host 头域新添状态码,增加了100在内,以及24个错误状态的状态响应码带宽优化及网络连接,引入了range头域,允许只请求资源的某个部分,即返回码是206(Partial Content),方便了开发者自由的选择以便于充分利用带宽和连接
五、HTTP/2.0
多路复用,降低开销(一次TCP连接可以处理多个请求), 一个连接里面并发处理请求,不像http1.1在一个tcp连接中各个请求是串行的;二进制格式,解析错误少,更高效,便于计算机操作(HTTP/1.X解析基于文本);头部压缩,在1.0版本后,增加了header头信息,而当发送多个请求时,头部很多相同或者类似,在2.0版本则通过HPACK算法把 header 进行了压缩这样数据体积就更小,在网络上传输就更快。数据流,每个请求或回应的所有数据包,称为一个数据流(Stream)。每个数据流都标记着一个独一无二的编号,规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数,这样可以客户端对数据流定义优先级,便于服务端优先响应
其中,1.0和1.1最常用,0.9几乎不用(旧),2.0比较少用(更新代价大)
四、知识点:HTTP请求头 和 响应头⭐⭐⭐
一、如图
请求标头:

响应标头:

二、常见的标头
你了解 HTTP请求头和响应头吗?也许我们很多的地方都使用过,但是因为没有做过一个完整的归纳我们了解的就不多。但是实际上我们随处可见,比如我们发送请求返回的资源我们需要判断是使用资源下载还是渲染页面。比如我们的判断缓存的是否过期使用的参数,比如我们跨域问题中等等的呢场景中!下面我们来看一点常见的吧!
请求 Header 解释 示例 Accept 指定客户端能够接收的内容类型 Accept: text/plain, text/html Accept-Charset 浏览器可以接受的字符编码集。 Accept-Charset: iso-8859-5 Accept-Language 浏览器可接受的语言 Accept-Language: en,zh Accept-Ranges 可以请求网页实体的一个或者多个子范围字段 Accept-Ranges: bytes Cache-Control 指定请求和响应遵循的缓存机制 Cache-Control: no-cache Connection 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) Connection: close Cookie HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 Cookie: $Version=1; Skin=new; Content-Length 请求的内容长度 Content-Length: 348 Content-Type 请求的与实体对应的MIME信息 Content-Type: application/x-www-form-urlencoded Date 请求发送的日期和时间 Date: Tue, 15 Nov 2010 08:12:31 GMT Expect 请求的特定的服务器行为 Expect: 100-continue Host 指定请求的服务器的域名和端口号 Host: www.zcmhi.com If-Match 只有请求内容与实体相匹配才有效 If-Match: “737060cd8c284d8af7ad3082f209582d” If-Modified-Since 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT If-None-Match 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 If-None-Match: “737060cd8c284d8af7ad3082f209582d” If-Range 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag If-Range: “737060cd8c284d8af7ad3082f209582d” Range 只请求实体的一部分,指定范围 Range: bytes=500-999 Referer 先前网页的地址,当前请求网页紧随其后,即来路 Referer:http://www.zcmhi.com/archives/71.html User-Agent User-Agent的内容包含发出请求的用户信息 User-Agent: Mozilla/5.0 (Linux; X11)
响应 Header 解释 示例 Accept-Ranges 表明服务器是否支持指定范围请求及哪种类型的分段请求 Accept-Ranges: bytes Age 从原始服务器到代理缓存形成的估算时间(以秒计,非负) Age: 12 Allow 对某网络资源的有效的请求行为,不允许则返回405 Allow: GET, HEAD Cache-Control 告诉所有的缓存机制是否可以缓存及哪种类型 Cache-Control: no-cache Content-Encoding web服务器支持的返回内容压缩编码类型。 Content-Encoding: gzip Content-Language 响应体的语言 Content-Language: en,zh Content-Length 响应体的长度 Content-Length: 348 Content-Location 请求资源可替代的备用的另一地址 Content-Location: /index.htm Content-MD5 返回资源的MD5校验值 Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== Content-Range 在整个返回体中本部分的字节位置 Content-Range: bytes 21010-47021/47022 Content-Type 返回内容的MIME类型 Content-Type: text/html; charset=utf-8 Date 原始服务器消息发出的时间 Date: Tue, 15 Nov 2010 08:12:31 GMT ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d” Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT Location 用来重定向接收方到非请求URL的位置来完成请求或标识新的资源 Location: http://www.zcmhi.com/archives/94.html Set-Cookie 设置Http Cookie Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
面试回答🙋♂️🙋♂️🙋♂️
在我们发送HTTP请求或者接收响应的时候,HTTP信息都会包含相应的头部信息用来告诉我们一些设置信息。比如我们的请求头中会包含 Cache-Control 用来指定请求和响应遵循的缓存机制,Connection 用来指定是否长链接,还有 Cookie ,Host ,Expect等。
同样的对于响应头也是,Content-Type 返回内容的类型让我们的浏览器判断是渲染HTML文件,还是打开资源下载,Expires 表示响应过期的日期和时间,Last-Modified 表示请求资源的最后修改时间等等。
五、知识点:说说你知道的 HTML5 的新特性 ⭐⭐⭐
也许很多特性我们都使用到,甚至习以为常了,没有发觉它就是一种 HTML5 属性,下面我们来认识和学习有哪些是吧!
1. 新的媒体标签
-
video

-
audio

2. 新的内容元素
为了便于页面的格式化,以及可阅读性,语义化等,定义了新的标签
标签 描述 < hrader > < /header > 定义了文档的头部区域 < footer > < /footer > 定义了文档的尾部区域 < nav > < /nav > 定义文档的导航 < section> < /section > 定义文档中的节(section、区段) < article >< /article > 定义页面独立的内容区域 < aside >< /aside > 定义页面的侧边栏内容 < detailes >< /detailes > 用于描述文档或文档某个部分的细节 < summary >< /summary > 标签包含 details 元素的标题 < dialog >< /dialog > 定义对话框,比如提示框
具体使用如图:

3. 新的表单输入控件类型
输入类型 描述 示范 color 主要用于选取颜色 
date 从一个日期选择器选择一个日期 
datetime-local 选择一个日期和时间 (无时区) 
email 包含 e-mail 地址的输入域 
month 选择一个月份 
number 数值的输入域 
range 一定范围内数字值的输入域 
search 用于搜索域 
tel 定义输入电话号码字段 
time 选择一个时间 
url URL 地址的输入域 
week 选择周和年 
4. 绘画
标签 描述 < canvas > 定义使用 JavaScript 的图像绘制。 < svg > 定义使用 SVG 的图像绘制。
5. 其他功能标签
标签 描述 progress 进度条。< progress max=“100” min=“1” value=“20”> datalist 文本域下拉提示。 detail 展开菜单。 mark 标注。 time 数据标签,给搜索引擎使用,主要日期标签。 ruby 对某些内容进行注释。 command 按钮。
推荐文章
六、知识点:三次握手 ⭐⭐⭐⭐⭐
开始:
A(TCP客户端进程)主动打开,离开close状态,B(TCP服务端进程)被迫离开close状态,进入listen状态
第一次握手:
首先A向B发送连接请求报文段,这时首部中的同步位 SYN = 1(主机B由SYN=1知道,A要求建立联机),同时选择一个初始序号 seq = x。A进入SYN-SENT(同步已发送)状态。该报文不包含应用层数据
第二次握手:
首先B收到A的请求报文段后,如果同意建立连接,则向A发送确认。在确认报文段中把 SYN位 和 ACK位 都置1(主机A由 请求报文 SYN=1 知道,B要求建立联机,并且返回ACK=1,表示收到上一个SYN的确认),确认号 ack = x+ 1,同时也为自己选择一个初始序号 seq = y。这时B进入SYN-RCVD(同步收到)状态。该报文不包含应用层数据
第三次握手:
A收到后,再次给B发送确认,确认报文段的ACK置1,确认号ack = y + 1,而自己的序号seq = x + 1。这时,TCP连接已经建立,A进入ESTABLISHED(已建立连接)状态。该报文可以包含应用层数据
面试回答🙋♂️🙋♂️🙋♂️
补充:
TCP标志位:
- SYN(synchronous建立联机)
- ACK(acknowledgement 确认联机)
- seq(Sequence number 顺序号码)
- ack(Acknowledge number 确认号码)
深化文章:计算机网络-运输层(UDP/TCP协议)
七、知识点:GET 和 POST的区别 ⭐⭐⭐⭐⭐
一、GET和POST
GET和POST,两者是HTTP协议中发送请求的方法
1、GET
GET方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据
2、POST
POST方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用
本质上都是TCP链接,并无差别
二、W3C上讲述的区别
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET产生的URL地址可以被Bookmark,而POST不可以。
- GET请求会被浏览器主动cache,而POST不会,除非手动设置。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制的,而POST没有。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
- GET参数通过URL传递,POST放在Request body中
下面我们看点更加深入的分析
三、具体post和get的区别分析
1、url可见性:
get,参数url可见
post,url参数不可见
get把请求的数据放在url上,即HTTP协议头上,其格式为:以?分割URL和传输数据,参数之间以&相连;post把数据放在HTTP的包体内(requrest body
2、传输数据的大小:
get一般传输数据大小不超过2k-4k
post请求传输数据的大小根据php.ini 配置文件设定,也可以无限大
get提交的数据最大是2k(原则上url长度无限制,那么get提交的数据也没有限制咯?限制实际上取决于浏览器,浏览器通常都会限制url长度在2K个字节,即使(大多数)服务器最多处理64K大小的url,也没有卵用);
post理论上没有限制。实际上IIS4中最大量为80KB,IIS5中为100KB**
3、 数据传输上:
get,通过拼接url进行传递参数
post,通过body体传输参数
GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
POST产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)**
4、后退页面的反应:
get请求页面后退时,不产生影响
post请求页面后退时,会重新提交请求
GET在浏览器回退时是无影响的,POST会再次提交请求
5、缓存性:
get请求是可以缓存的
post请求不可以缓存
GET请求会被浏览器主动cache,而POST不会,除非手动设置
6、 安全性:
都不安全,原则上post肯定要比get安全,毕竟传输参数时url不可见,但也挡不住部分人闲的没事在那抓包玩,浏览器还会缓存get请求的数据。安全性个人觉得是没多大区别的,防君子不防小人就是这个道理。对传递的参数进行加密,其实都一样
7、编码方式
GET请求只能进行url编码,而POST支持多种编码方式
8、是否有历史记录
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
9、接收类型
GET只接受ASCII字符的参数的数据类型,而POST没有限制
那么,post那么好为什么还用get?get效率高!
面试回答🙋♂️🙋♂️🙋♂️
本质上两者没有啥区别,都是基于同一个传输层协议,但细节上有所不同。
首先最直观的是,GET请求会把参数拼接在URL中,这样不太安全,敏感信息会直接被读取到,同时传递的参数长度有限,并且请求信息会被保存在浏览器记录中,而这些问题再POST中都不会,POST请求会把参数保存在请求体中,对参数长度也没有要求,也不会被浏览器记录。
其次还有对于参数类型GET只能接收ASCII字符,而POST没有限制。GET在浏览器回退时是无害的,而POST会再次提交请求。
八、知识点:HTML5 离线存储 ⭐
一、 为什么?
那么为什么需要使用离线缓存呢?那么我们肯定看他能给我们带来什么便利:
- 浏览器缓存资源,后期即使没有网络也可以展示页面
- 在有网络的时候使用本地资源,加快加载速度,优化用户体验
- 减轻服务器负担,当不需要更新时用户直接读取离线资源,而不用每次向服务端请求
二、 原理
首先关键在于使用 manifest 属性,在需要缓存的的 HTML 根节上添加该属性,属性值是一个 . manifest文件,浏览器在发现有该属性就会去请求相应的 manifest 文件。
<!DOCTYPE HTML>
<html manifest = "cache.manifest">
...
</html>
- 1
- 2
- 3
- 4
如果是第一次访问,浏览器就会根据 manifest 文件的内容,下载相应的资源,并且缓存起来,便于离线使用。如果是第二次,浏览器就会先比较新的 manifest 文件和旧的是否发送改变,如果没有,直接读取使用之前缓存的资源。
manifest文件就像是一个缓存清单文件,后缀是一个 . appcache 文件。
CACHE MANIFEST
#v0.11
CACHE:
js/app.js
css/style.css
NETWORK:
*
FALLBACK:
/demo/ /404.html
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- CACHE:表示需要离线存储的资源列表
- NETWORK:表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。如果在CACHE和NETWORK中有一个相同的资源,那么这个资源还是会被离线存储,CACHE的优先级比NETWORK高。 同时可以使用 *,表示除CACHE 外的所有其他资源/文件都需要因特网连接。
- FALLBACK:在此标题下列出的文件规定当页面无法访问时的替代页面。,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问offline.html。
上面的清单文件表示的意思就是:首次加载的时候 js/app.js css/style.css这两个文件会被缓存,而除这两个文件以外的文件都需要联网获取,而如果没有网络获取,就会用 “404.html” 替代/demo/目录中的所有文件。
使用 window.applicationCache.status 可以查看缓存的当前状态
- 0(UNCACHED) : 无缓存
- 1(IDLE) :闲置(即应用缓存未得到更新,是最新的状态)
- 2 (CHECKING) :检查中(即正在下载描述文件并检查更新)
- 3 (DOWNLOADING) :下载中(即应用缓存正在下载描述文件中指定的资源。)
- 4 (UPDATEREADY) :更新完成
- 5 (IDLE) :缓存过期(即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存)
在 window.applicationCache 中还有很多方法,比如update() 来主动更新缓存等。
离线缓存和浏览器缓存的区别:
- 是针对整个应用,浏览器缓存是针对单个文件
- 可以断网使用
九、知识点:同步和异步的区别 ⭐⭐⭐⭐⭐
一、同步
- 同步是指一个进程在执行某个请求的时候,如果该请求需要一段时间才能返回信息,那么这个进程会一直等待下去,直到收到返回信息才继续执行下去。
二、异步
- 异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有信息返回的时候会通知进程进行处理。所以不会等待。
同步的话会造成阻塞,当请求一直没有返回,就会导致后面的步骤一直没发进行,但是异步不会,就算某个请求阻塞了,也不会影响其他的。而我们在根据选择是同步还是异步的时候,需要判断的是请求之间的资源是否有依赖关系。当有依赖关系的haul我们就要使用同步了。
十、知识点:DNS域名查找的过程 ⭐⭐⭐
一、首先我们看看 DNS 的定义:
DNS是域名系统 (Domain Name System) 的缩写,它是由解析器和域名服务器组成的,又名“域名解析服务器”,把机器名称转换为IP地址的系统,靠它把你要访问的网址找到然后把信息送到你电脑上。
二、那么为什么需要域名系统呢?直接用机器名称不行吗?
- 首先 IP 地址是32位 ,如果 IP6 就是128位 ,这都是定长度的。而域名的长度是不定的,机器处理起来比较麻烦。
- 其次域名也便于大家阅读和理解,也在一定长度上保护的服务器的 IP地址。
三、那么 DNS域名系统是如何工作的呢?
- 首先会向浏览器缓存查询有没有这个域名的IP缓存,查询没有
- 就会去操作系统的缓存中去查询有没有这个域名的 IP缓存,查询没有
- 就会本地域名服务器发起查询,首先先看是否在本地域名服务器的配置区域资源中,在的话返回,不在的话看看缓存有没有,查询没有
- 就会去根域名服务器发起查询(根域名服务器有13个),判断这个域名归谁管,返回该顶级域名服务器的IP
- 本地域名服务器向顶级域名服务器进行查询。
- 顶级域名服务器告诉本地域名服务器,下一步查询权限服务器的IP地址
- 本地域名服务器向权限服务器进行查询
- 权限服务器告诉本地域名服务器所查询的主机的IP地址
- 本地域名服务器最后把查询结果告诉主机
四、域名查询
域名查询中两种常见的方式
主机向本地域名服务器查询
- 一般是 递归查询
- 当在本地的域名服务器没有找到时,本地服务器就会以DNS的身份向其它的根域名服务器继续向其它根域名发出查询请求报文,没找到时,其它的根域名服务器会以DNS的身份向其它的根域名服务器继续向其它根域名发出查询请求报文,直到找到或报错。
比如:你要找一本书,去到图书馆,你问图书馆管理员A书在哪,管理员A找到了给你,没找到管理员A就会向管理员B问这本书,管理员B就会去找,找到了给管理员A,管理员A再给你。找到最后没找到就很遗憾告诉你,没有这本书。
本地域名服务器向根域名服务器发送请求
- 一般是 迭代查询
- 一般是本地的域名服务器向其它根域名服务器发送请求,要么返回所找的ip,要么返回告诉本地的域名服务器去哪找。
比如:你要找一本书,去到图书馆,你问图书馆管理员A书在哪,管理员A找到了给你,没找就告诉你可能在管理员B那,你得自己去问管理员B,管理员B找到了给你,没找到的话就告诉你可能在管理员C那…找到最后没找到就很遗憾告诉你,没有这本书。
那么其实本地的查询方式,取决于最初查询请求报文的设置是哪一种方式

五、缓存

深入文章:计算机网络-应用层的DNS与HTTP
十一、知识点:常见的网络状态码 ⭐⭐⭐⭐⭐
这个可以说面试的常客了~
总的来说分为五类:
- 1XXX:表示通知请求
- 2XXX:表示成功
- 3XXX:表示重定向
- 4XXX:表示客户端错误
- 5XXX:表示服务端错误
下面看看一些比较常遇到的,代表什么意思吧~
100:继续 ;
101:切换协议
200:请求成功
204:内容为更新
301:永久移动 / 302 :临时移动
304:未修改(缓存)
400:请求语法错误
401:身份验证
403:没有权限,拒绝
404:没有找到资源
405:其求方法禁止
501:服务器不支持
503:服务器维护/超载
十二、知识点:七层协议 ⭐⭐⭐⭐⭐

计算机组成
一、知识点:线程和进程 ⭐⭐⭐⭐⭐
一、 进程:
为了表达程序的并发过程的变化,但程序的静态性和顺序性无法描述该过程,所以有了进程这个定义
进程是系统资源分配的最小单位,是程序的一次运行,有自己的独立空间
二、线程:
为了提高进程的并发性,进一步提高系统的吞吐量和效率
三、 线程和进程的区别:
- 一个程序至少有一个进程,一个进程至少有一个线程.
- 进程是拥有资源的基本单位,线程是调度和分派的基本单位,线程间能共享该进程的资源。
- 都具有并发性,但线程的划分小于进程,有效的提高了效率
- 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。而是归属于的进程的下面。
- 每一个进程能单独存在,但线程必须依赖于进程,不能独立存在。
- 线程的意外终止会影响整个进程的正常运行,但是一个进程的意外终止不会影响其他的进程的运行。
- 进程切换开销大,效率低,线程切换开销小,效率高

深化文章:计算机基础-进程与线程以及携程
二 、 知识点: IP 的认识 *⭐
首先 IP 是什么呢?我们整个互联网是一个单一,抽象的网络。而 IP 地址是互联网上每一台主机的,全球唯一的标识符,在整个互联网中是唯一的。
三、知识点:死锁 *⭐
VUE
一、知识点:说说Vue的优点和特点 ⭐⭐⭐
一、优点
首先是双向绑定
这也是我们使用框架的一大优势,VUE使用MVVC架构,在 VUE.2X 中使用Object.definProperty () 来劫持绑定数据,在VUE.3X 中使用 Proxy 劫持。如果按照最初的开发,我们前端开发不仅仅是需要完成业务代码的实现,同时还需要对每个 DOM元素进行获取绑定时间和数据。而双向绑定使得我们只需要专注于业务代码的实现上。简单易学
VUE以简单,易上手的特点,在国内很多企业得到使用。同时作为国人开发的框架,中文文档,相关的论坛,生态完善,也便于我们学习和遇到问题的解决方案。虚拟DOM
使用虚拟DOM,结合DIFF 算法能减少性能损耗。他会把我们多次的操作合并为一次,推送到真实的DOM。另外补充,我们说虚拟DOM减少损耗是有条件,是指在频繁操作的情况下,不然肯定简单的获取操作最快的,期间没有很多计算等处理。组件化的思想
实现组件的封装,我们往往是使用组件开发的思想去封装组件,这样不仅便于复用也好维护修改!
二、缺点
生态不够完善
相比 angular 和 react 来说,生态环境较为不足,在构建大型的应用方面 ,企业使用 react 的比较多。而中小型企业使用 VUE 比较多。
三、你使用过reat吗,两者有什么区别和优缺点?
没有,其实我感觉技术都是共同的,理解透彻一门,再去学习别的就能很快的上手运用。感觉贪多嚼不烂。
据我了解的话,VUE 和 React 的共同点是,都是组件化的思想,虚拟DOM,数据绑定,而不同点在于,首先两者的设计思想不同,前者定位降低前端开发的门槛,而后者推崇函数式编程。另外在 React 中,是使用 JSX的写法,把HTML 和 CSS都写入到 JavaScript 中。还有的就是他们的 DIFF算法实现也不太一样。
一、知识点:MVVM 架构 和 MVC 架构 ⭐⭐⭐⭐⭐
一、MVVM 架构 和 MVC 架构
1、MVC
MVC 全名是 Model View Controller 的简写,是模型 (model)-视图 (view)-控制器 (controller) 的缩写
在这其中:
- Model (模型):负责处理数据的逻辑和从数据库存取数据,也就是数据存取
- View (视图):负责展示数据,UI界面,用户交互,也就是用户界面
- Controller (控制器): 处理业务逻辑

视图层 发送用户操作给控制器, 控制器 处理业务逻辑,完成后告诉模型更改状态,模型 将新的的数据发送给视图层,用户得到操作的反馈。
在 MVC 架构中,所有的通信都是单向的
2、MVVM
MVVM 全名是 Model-View-ViewModel 的简写,是模型 (model)-视图 (view)-视图模型 (viewmodel) 的缩写
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件, 也就是用户界面
- 视图模型(ViewModel):框架封装的核心,它负责将数据与视图关联起来

在 MVVM架构中,通过是视图模型来实现 视图层 和数据层的双向绑定,当视图层的数据发生变化,就会通过视图模型告诉数据层更新,同样当数据层发生变化也会通知视图层更新。
在 MVVM 架构中,所有的通信都是双向的
二、那么 MVVM 和 MVC 有什么区别呢?
可以说 MVC 是后端的思想,而 MVVM是前端的思想,而 MVVC 相比 MVC 最大的区别就是是实现了数据的双向绑定
在别处看到这张图,讲的很不错

二、知识点:说说在Vue中的双向绑定 ⭐⭐⭐⭐⭐
上面我们讲了MVVM 和 MVC 模型,下面我们看看,MVVM 模型带来的双向绑定。
一、首先什么是双向绑定呢?
简单来说,就是我们的一个表单,当我们修改 data 的数据,而视图中的数据也发生改变,而当我们修改视图的数据,data 的数据也会跟着改变。
二、而双向绑定的原理是什么呢?
其实就是我们上面的 MVVM 架构
三、那么双向绑定具体是如何实现的呢?
实现双向绑定,最主要是数据绑定,常见的有下面几种
发布者-订阅者模式(backbone.js)
脏值检查(angular.js)
数据劫持(vue.js)
这里我们看看 vue 中的实现实现双向绑定的原理。
主要核心来说是通过 Object.definProperty ()来对 data 中的每一个属性 进行 get , set 的拦截。下面是一个基于 Object.definProperty () 的简单实现。
//极简版的双向绑定
<input type="text" id = 'txt_id'/>
<p id='p_id'></p>
<script type="text/javascript">
let _xxObj = {};
Object.defineProperty( _xxObj , "xx_val", {
get: function() {
console.log('gggg');
return 'xx123'
},
set: function( _n ) {
console.log(_n);
document.getElementById('txt_id').value = _n
document.getElementById('p_id').innerHTML = _n
}
});
document.addEventListener('keyup', function(e){
// console.log(e.target.value);
_xxObj.xx_val = e.target.value
});
// //写入触发set
// _xxObj.xx_val = 11;
// //读取触发get
// console.log(_xxObj.xx_val);
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
效果如图:

可以说我们上面完成简单的双向绑定,但是这里是直接操作dom的,要是我们有一千,一万个dom,那就炸裂了,所以说是简单版本。
而在vue.js 除了采用 数据劫持 外,还结合了 发布者-订阅者模式 的方式
通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
思路上是首先有一个数据的监听器 Observer 劫持并且监听所有属性,当发生变化时,就会通知订阅者 Watcher 是否需要更新视图(因为订阅者不止一个所以有一个消息订阅者 Dep 来收集订阅者,在监听器和订阅者之间统一管理)。
同时还有一个指令解析器Compile,,Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,当订阅者Watcher接收到,就会执行对应的更新函数,从而更新视图。

面试回答🙋♂️🙋♂️🙋♂️
主要要回答到下面的两点:
- 首先双向绑定是通过 MVVM 架构来实现的,MVVM 架构分为 视图层 / 视图模型 /和数据层,通过视图模型来实现 视图层 和数据层的双向绑定,当视图层的数据发生变化,就会通过视图模型告诉数据层更新,而数据层同理。
- 在这里主要使用到 Object.defineProperty() 这个属性,当一个Vue实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0使用proxy ),用来劫持各个属性的setter,getter,并且在数据变动时发布消息给订阅者,触发相应的监听回调,从而更新视图。
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
参考文章:剖析Vue原理&实现双向绑定MVVM
三、知识点:Vue 的生命周期 ⭐⭐⭐⭐⭐
关于 VUE 的生命周期的文章很多,总结来说,学习 VUE 的生命周期就是为了更好的了解在 VUE实例 在运行的过程,什么时候创建,什么时候初始化数据等过程,这样我们才能更好的决定什么时候做什么事情!
一、首先我们先了解各个生命周期的作用
首先在 vue 生命周期中有 十个阶段:
1、beforeCreate(创建前)
在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
因为 data 和 methods 中的数据都还没有初始化,常常在该阶段执行与 vue 数据无关的事件,比如我们的 loading 等待事件。
2、created(创建后)
在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。
在该阶段已经完成 data 和 methods 的初始化了,只是页面还未渲染,可以在该阶段来发起请求获取数据等,以及操作 data 和 调用 methods 等
3、beforeMount(挂载前)
在挂载开始之前被调用:相关的 render 函数首次被调用。
该钩子在服务器端渲染期间不被调用
4、mounted(挂载后)
实例被挂载后调用,这时 el 被新创建的 vm.$ el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$ el 也在文档内。
注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick
该钩子在服务器端渲染期间不被调用
这时候 vue实例完成初始化,且挂载渲染到页面了,最早我们可以在这个阶段来操作 页面上的 DOM节点。
5、beforeUpdate(更新前)
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
该阶段此时实例中的数据已经是最新的啦,但是页面的还未更新
6、update(更新后)
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。
注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick
该钩子在服务器端渲染期间不被调用
避免在这个阶段更改状态的,因为这样可能会导致更新无限循环,曾经我做过个获取表格的高度,如果页面大小发送改变就更新表格的大小,在该阶段执行导致了页面的无限循环,结果死机啦~
7、activated(激活前)
被 keep-alive 缓存的组件激活时调用。
该钩子在服务器端渲染期间不被调用
8、deactivated(激活后)
被 keep-alive 缓存的组件失活时调用。
该钩子在服务器端渲染期间不被调用
9、beforeDestory(销毁前)
实例销毁之前调用。在这一步,实例仍然完全可用。
该钩子在服务器端渲染期间不被调用
10、destoryed(销毁后)
实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
该钩子在服务器端渲染期间不被调用
面试回答🙋♂️🙋♂️🙋♂️
面试官问到这个问题的时候该怎么回答呢?
Vue 的生命周期也就是 Vue实例从创建到销毁的过程,总共分为8个阶段 创建前/后,载入前和后,更新前和后,销毁前和后。
-
创建前和后:
- 在 beforeCreated 阶段,Vue 实例刚在内存中被创建出来,data 和 methods 还未初始化,都为 undefined
- 在 created 阶段,vue实例的 data 和 methods 就已经存在了,但是DOM 还未挂载渲染
-
载入前和后:
- 在 beforeMount 阶段,DOM挂载完成,数据也初始化完成,但是数据的双向绑定还是显示{{}},这是因为 Vue采用了Virtual DOM(虚拟Dom)技术。先占住了一个坑
- 在 mounted 阶段,DOM挂载完成,上一个阶段留的萝卜坑也补上了
-
更新前/后:
- 在 beforeUpdate 阶段,此时实例中的 data 以及是最新的了,但是界面上显示的数据还是旧的,此时还没有开始重新渲染DOM节点
- 在 update 阶段,此时实例中的 data 和界面上显示的数据都是新的了,已经完成了 DOM节点 的渲染
-
销毁前/后:
- 在 beforeDestroy 阶段, Vue实例销毁之前调用。在这一步,实例仍然完全可用。
- 在 beforeDestroy 阶段 ,Vue实例所有的事件监听以及和 dom的绑定都解除了
四、知识点:vue 中的 ref ⭐⭐⭐
ref
常用来辅助开发者,获取 DOM 元素或组件的引用,以及用于在父子组件中获取对方的某个元素进行取值,调用方法等。
在每个 Vue 的组件实例上, 都包含一个 $refs 对象, 里面存储着对应的 DOM 元素或组件的引用
- 默认情况下,组件的 $refs 指向一个空对象
- 如果想要使用 ref 引用页面上的组件实例,则可以按照如下方式:
- 使用ref属性,为对应的组件添加引用名称
<my-counter ref="counterRef"></my-counter>
<button @click="getRef">获取 $refs 引用</button>
methods: {
getRef(){
// 通过 this.$refs. 引用的名称,可以引用组件的实例
console.log(this.$refs.counterRef)
// 引用到组件的实例之后,就可以调用组件上的methods方法
this.$refs.counterRef.add()
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
这个方法可以说很便利,但是不要太依赖了,往往在不能通过其他方法获取的时候回才比较建议使用,毕竟我们因该尽量减少添加,而是复用可以复用的部分。
$refs 只会在组件渲染完成之后生效,并且不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问
tip:如果获取不到的时候,可以试一试使用nextTick
五、知识点:在 Vue 中 $nextTick 的使用 ⭐⭐⭐
$nextTick
官方解释:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
为啥需要这个呢?
因为 vue 中不是数据一发生改变就马上更新视图层的,如果这样会带来比较差的性能优化,而是在 vue 中会添加到任务队列中,待执行栈的任务处理完才执行。所以 vue 中我们改变数据时不会立即触发视图,但是如果我们需要实时获取到最新的DOM,这个时候可以手动调用 nextTick。
$nextTick 的机制
曾经,刚开始吃过大亏,当时就是因为还没有获取到更新(v-show)后状态的 DOM 元素,导致事件没能绑定到对应的元素上,测试了半天都没效果,后面才发现是这个问题。引以为戒!!!!
六、知识点:关于VUE - Router *⭐⭐⭐
上次被面到这个东东,突然不知道怎么回答了,虽然平时一直用东西,但是要说好像就说不出啥~这里回顾一下 vue-router 包含什么吧!
一、路由常见的属性
首先我们先看路由的两个属性 $router 和 $route 然后说说他们的作用,再结合一些场景的问题。
$router
- $router.app
- $router.mode
- $router.currentRoute
- router.addRoutes(routes)
- router.beforeEach(to,from,next)
- router.afterEach()
- router.go(n)
- router.push( location )
- router.replace( location )
- router.back()
- router.forward()
- router.resolve(location)
- router.onReady(callback,[errorCallback]){}
- router.onError(callback)
$route
- $route.path
- $route.query
- $route.params
- $route.hash
- $route.fullPath
- $route.name
- $route.matched
- $route.redirectedFrom
二、常见场景下的解决问题
知识点:VUE - Router 中路由拦截 *⭐
七、知识点:vue路由传参 ⭐
前段时间在上班的时候,发现有个路由跳转还携带了参数,突然才意识到当年自己vue学又多烂还忘了很多东西,有空真的应该完整的去回顾一次的
也回想当年做过一个项目,当时的做法多傻,当时是一个视频网站,需要点击这个视频就有相关的跳转到该视频的播放页面,当时我居然把那个id存储到localstoram里面,在那个页面再去读取,妈呀,现在回想有好几种方式实现,扎做都不会沦落到用localstorage来
回到正题~
路由传参方式可划分为 params 传参和 query 传参,而 params 传参又可分为在 url 中显示参数和不显示参数两种方式
方式A:这种需要在路由配置好可以传递参数XXX的,不是最方便的
路由配置
{
path: '/child/:XXX',
component: Child
}
父组件
<router-link to = "/child/XXX"></router-link>
子组件读取
this.num = this.$route.params.XXX
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
方式B:这种同样需要在路由配置好可以传递参数XXX的,不过是用到push方法的
路由配置
{
path: '/child/:XXX',
component: Child
}
父组件
this.$router.push({
path: `/child/${XXX}`
})
子组件读取
this.num = this.$route.params.XXX
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面两种方式都会在地址显示出传递的参数,类似get请求~~
方式C:这种不需要在路由配置好根据路由的名称,需要保持一致
路由配置
不需要配置,但是子组件的name必须与父组件传递的路由一致
父组件
this.$router.push({
name: 'B',
params: {
XXX: '妈呀'
}
})
子组件读取
this.num = this.$route.params.XXX //妈呀
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
方式D:这种不需要在路由配置好根据路由的名称,通过query来传递
路由配置
不需要配置,但是子组件的name必须与父组件传递的路由一致{
父组件
this.$router.push({
path: '/child',
query: {
XXX: '妈呀'
}
})
子组件读取
this.num = this.$route.query.XXX
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
总的来说使用方式C和D最为多,毕竟不需要对路由配置做修改
八、知识点:computed 和 watch 的区别 ⭐⭐⭐
一、首先我们先来考虑为什么需要使用到这两者,他们解决什么问题?
他们都是用来当我们的数据发生改变的时候,所有和该数据有关的数据都自动更新,执行我们定义的方法。不同于 methods 是需要相关的方法和交互来实现调用执行的。
二、computed 和 watch 以及 methods 的区别
1、性质上
methods :定义函数,手动调用
computed:计算属性,return 返回结果,自动调用
watch: 观察,监听,发生改变就调用
2、使用场景
methods :一般不处理数据的逻辑,用于获取数据,和改变状态等情况
computed:多用于一个数据受多个数据影响的情况,具有 缓存的特性,避免每次重新计算
watch: 多用于一个数据影响多个数据的情况,且类似异步等情况的时候建议使用
3、执行时间
computed 和 methods 的初始化是在 beforeCreated 和 created 之间完成的。(以及Props, data都是)
4、缓存
computed:有缓存,重新渲染时,要是值没有改变,会直接返回之前的
watch:无缓存,重新渲染时,要是值没有改变,也会执行
5、是否异步
computed:不支持异步,但异步监听的时候,无法监听数据变化
watch:支持异步监听
三、computed 和 methods 的区别
无论是定义为一个计算属性还是一个函数方法,对于结果来说都是一样的,不同点在于,计算属性是根据依赖值变化才发生改变,而函数方法需要调用执行。
九、知识点:v-show 和 v-if 的区别 ⭐⭐⭐⭐⭐
1、本质上
- v-show 是把标签里的 display 设置为 none,所以页面上是可见的
- v-if 是动态的操作DOM元素,页面上不可见的
2、性能上
- 要是需要频繁的操作的话,肯定是 v-show ,因为他只是操作css的值,不会频繁触发重排。但是 v-if 是不断的向 DOM树添加或删除元素,在比较少改变的时候比较合适。
- v-show 无论任何条件,初始都会渲染,v-if是惰性的,如果初始条件为 false,初始不会渲染 DOM ,为 true 才会渲染。因此 v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销
十、知识点:vue 的 Scoped 原理 ⭐⭐⭐
Vue的作用域样式 Scoped CSS 的实现思路如下:
- 为每个组件实例生成一个能唯一标识组件实例的标识符,记作 InstanceID;
- 给组件模板中的每一个标签对应的 Dom 元素添加一个标签属性,格式为 data-v-实例标识(示例:< div data-v-e0f690c0=“” >);
- 给组件的作用域样式 < style scoped> 的每一个选择器的最后一个选择器单元增加一个属性选择器
特点
1、 将组件的样式的作用范围限制在了组件自身的标签
即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的 css 选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);
因为它给选择器的最后一个选择器单元增加了属性选择器 [data-v-实例标识] ,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性 data-v-子实例标识,又有能代表当前组件(父组件)的 签属性 data-v-父实例标识,子组件的其它非根元素,仅有能代表子组件的标签属性 data-v-子实例标识;
2、 如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;
原因:假设递归组件A的作用域样式中有选择器有后代选择器 div p ,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器 div p[data-v-当前递归组件实例的实例标识],对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然 div p[data-v-当前递归组件实例的实例标识] 不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;
十一、知识点:样式穿透 ⭐⭐⭐
我们在现在的开发中,往往会使用到各种的UI框架,但是有时候框架的样式并不是我们想要的,而且这时候我们去直接修改这个样式的时候,往往还会不生效,这是为什么呢?下面我们一一道来~~
一、上面我们看了 scoped 属性的一些特性,这里简单回顾:
我们在写组件的时候,往往会加上这个属性,它的作用是不污染全局样式,只在当前组件的范围内有效。它具体是如何实现的呢?

我么可以看到每一个元素都加上了一个标识符,这个的实现基于 PostCss。所以我们在使用第三方的组件库的时候,就无法简单的覆盖了,我们无法读取到组件里的。所以这里就到了我们这次的主题:样式穿透
二、用法:
父元素 ::v-deep 内部元素(通过f12查看元素类名)
- 1
是不是简简单单,其实除了上面这种方式外,还有下面两种,但是上面这种基本可以解决我们遇到的问题了~~
//如果使用的是css,可以用下面这种
外层容器 >>> 组件 {}
//但在css预处理器中用上面这种是无法生效的,类似在scss和less中,我们可以用下面这种。
外层容器 /deep/ 组件 {}
- 1
- 2
- 3
- 4
- 5
十二、知识点:关于vue中的插槽使用⭐⭐⭐
前几天做个业务的时候用到插槽,才发现对这个还不太熟,这里来回顾一下基本的使用。
我们在开发中,往往会把一个复杂的页面分为多个组件去组合,或者当复用性比较高的也会提取为一个组件,但是当我们设计的组件还不能在某些特殊的情况满足我们的需求的时候,我们还需要添加额外的时候,那么该怎么办呢?总不可能再写多一个组件吧?这样的话,复用性也太低了。所以插槽就出现了。
当实际使用的组件不能完全的满足我们的需求的时候,我们就可以用插槽来分发内容,往我们的这个组件中添加一点东西。
基本的使用就是在子组件里面我们需要添加内容的地方加上
<slot></slot>
- 1
那么我们在父组件中使用该子组件的时候,就能直接在这个子组件中添加进入内容。这就是最基本的使用。
那么要是我们想在父组件中传递多个内容呢?这时候就有我们的对应使用的具名插槽了,我们可以对插槽命名狮子对应在子组件中的位置。
//我们可以在子组件的插槽中写入内容,也就是默认值,当我们在父组件中没有传递值的时候,就会使用默认值
<slot>我是默认的插槽值</slot>
//具名插槽
如下,我们在子组件中的插槽可以命名,这样我们在父组件中可以通过名字来确定位置
//子组件B
const BBorder = {
template: `我是儿子B
这里是头部
这里是默认
这里是尾部 `,
}
const Parents = {
methods: {
},
//实例化BBorder这个子组件
template: `我是父组件:{{msg}}
头部
A 9999999999999
尾部
`,
//注册两个子组件
components: {
BBorder
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
同样的,也能出传递值给到父组件中,但是我么要注意,父子组件的编译作用域是不同的,父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
从子组件中传递值到父组件
//子组件中
<slot :user="user">
{{ user.lastName }}
</slot>
//父组件中
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
基本的使用级就这样啦~~
十三、知识点:vuex的简单回顾 ⭐⭐⭐
今天回顾才发现对vuex的掌握,很多地方都忘了,太气人了,所以这里简单回顾一下vuex
一、首先我们肯定得看看vuex是什么的
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
也就是说,vuex是基于 vue 框架的一个状态管理库,那么什么是状态管理呢?
状态管理模式:简单来说就是集中式的存储管理组件的状态思想
二、那么 vuex 解决了什么?
我们思考一个场景:
比如我们有一个在线多人聊天的,类似微信的,我们有一个通知栏来告诉用户是否有未读信息,但是出现了通知栏偶尔会给出错误的通知,也就是用户收到一条新的未读消息的通知,但是当他们检查它是什么时,这只是他们已经看到的消息。
这是为什么呢?因为这里面
-
多个组件 / 实例依赖于同一状态,兄弟组件之间的状态是无法传递。
-
并且来自不同视图的行为当需要变更同一状态的时候, 经常需要通过事件等来变更和同步状态,当遗漏的时候就会出现有些地方状态更新错误。
var Vo= {}
var A = new Vue({
data: Vo
})
var B = new Vue({
data: Vo
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
比如某个数据,在实例A和实例B中使用了,而这个数据在实例A中修改了,如果我们不做处理,这个状态是不会更新到实例B中的,对于实例B来说,这个数据是没有发生改变的。小型的项目我们还比较好处理,对实例B更新就好了。
但是,当我们在做大型的开发的时候的话,往往会有多个实例 / 组件(A,B,C,D…)共享 一个数据,这种情况它们之间互相关联,导致数据的复杂性大大增加,数据的状态变得不可预测或难以理解。结果就是应用程序变得无法扩展或维护,变得臃肿难以维护及复用
所以我们需要使用 vuex 统一的管理(思想是实现 Flux 模式),不必考虑各处的处理,只需要考虑一个,且具有下面的优点:
- 全局管理
- 状态变更跟踪
- 规范化
- 便于调试
Flux 模式的基本思想:
- 单一数据来源
- 数据是只读的
- 更新是同步的
二、vuex 的好处
那么使用vuex的好处有哪些呢?
- 首先多层嵌套的组件、兄弟组件间的状态会更好管理维护,且使用 vuex 也是组件通信中的一种
- 减轻服务端压力,对于同一份数据集,请求会缓存,所有共用
- 当一个项目比较大型的时候,这是便于团队之间开发和维护的,和便于调试
那么我们知道为什么需要 vuex 和 vuex 的好处之后,我们来简单回顾一下他的使用吧。
二、五个核心的概念 state / getters / mutations / action / modules
在 vuex 中,核心就是 一个 “store” 仓库,包含我们应用中的大部分状态,同时提供包含 state / getters / mutations / action / modules等在内的管理调用方法。
1、state
state 的存储状态类似于就组件中data,是我们所有的集中数据的地方,访问 “store” 的状态可以使用两种方式 ,通过 this.$store.state属性 的方式或者 mapState 辅助函数。
//存储
const store = new Vuex.Store({
// 存储状态数据
state: {
count: 0,
info:{
name:"wenmo",
age:18
}
},
)
//读取方式
computed: {
count () {
return store.state.count
}
}
//下面这种是全局注册了store的
computed: {
count () {
return this.$store.state.count
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
辅助函数 mapState:当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余
computed: {
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
- 1
- 2
- 3
- 4
- 5
- 6
2、getters
getters 就是类似组件中的计算属性,可以简单理解为 state 的计算属性,依赖于 state 的值,state 发生改变就会同时改变
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
3、mutations
mutations的作用是用来修改 “store” 里面的值,并且是唯一的方式。
我们不能直接修改 store 容器中的状态,需要先在容器中注册一个事件函数,当需要更新状态时候要通过触发该事件来完成。
const store = new Vuex.Store({
state: {
name: "wenmo"
},
mutations:{
// 注册事件
addCount(state){
state.name = "WENMO"
}
}
})
//在实例中显式的调用执行
new Vue({
el: '#app',
store,
methods:{
// 显示的调用
handleAdd(){
this.$store.commit('addCount')
}
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
4. action
Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.action用于异步的修改state,它是通过muation间接修改state的。
若需要异步操作来修改state中的状态,首先需要action来注册事件,组件视图在通过 dispatch 分发方式调用该事件,该事件内部提交mutation中的事件完成改变状态操作,总之,通过action这个中介来提交mutation中的事件函数.
const store = new Vuex.Store({
state: {
name: "wenmo"
},
mutations:{
// 注册事件
setName(state){
state.name = "WENMO"
}
};
// 注册事件,提交给 mutation
actions:{
setAction(context){
setTimeout(() => {
context.commit('setName')
}, 1000)
},
}
})
//在实例中显式的调用执行
new Vue({
el: '#app',
store,
methods:{
// 显示的调用
handleAdd(){
this.$store.dispatch('setAction')
}
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
5. modules
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
6、关于这五个方法的总结
- state 对应的是在 store 中存储数据,类似组件中的 data
- getter 对应的是 store 中访问数据,类似组件中的 计算属性
- mutation 对应的是 store 中修改数据,并且是唯一的更新方式
- action 类似于 mutation,用来代替 mutation进行异步操作的
- modules 的作用是用来分割 store 的,每个 module 拥有自己的state、mutation、action、getters等
面试回答🙋🙋🙋
Vuex 是一个专为 Vue开发的状态管理模式,使用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。内部实现是 FLUX 模式,也就是约定数据的单向流动。
主要有五个属性,state / getter / mutation / action / modules ,分别是用来存储数据,访问数据,修改数据,进行异步操作的,和划分模块的。
基本回答这样就好了,等他询问里面的一些细节再来补充回答!比如为什么 mutation 是唯一的更新方式,mapState 辅助函数,为什么使用 VUEX 等。
推荐文章:
知识点: Flux 模式 ⭐
这个模式我们就不太深入了解了,我们主要来看看下面的个问题:
一、为了解决什么
解决的是一个:当应用程序中有多个组件共享数据时,它们互连的复杂性会增加到数据状态不再可预测或不可理解的程度。因此,应用程序变得无法扩展或维护。
举个例子就是,比如我们一个在线多人聊天,类似微信的,我们有一个通知栏来告诉用户未读信息,但是通知栏偶尔会给出错误的通知。用户将收到一条新的未读消息的通知,但是当他们检查它是什么时,这只是他们已经看到的消息。
这就是多组件之间状态管理的混乱,因为 MVC架构中没法保证数据的单向流动。
二、 有什么特点
单一事实来源,任何要在组件之间共享的数据,都需要保存在一个地方,与使用它的组件分开。这个单一的位置被称为“商店”。组件必须从该位置读取应用程序数据,而不是保留自己的副本以防止冲突或分歧。数据只可读,组件可以自由地从存储中读取数据。但他们不能更改存储中的数据,至少不能直接更改。相反,他们必须通知存储他们更改数据的意图,并且存储将负责通过已定义“更新”的函数进行这些更改。更新是同步的,我们根据上面的两个原则,就能很便利的追踪数据的状态变化和调试,但是如果是异步的话,我们就不能很好的判断执行的顺序。
三、具体使用
具体的应用就是我们常见的 VUEX 和 React 等中
十四、知识点:VUE过滤器 ⭐⭐⭐
麻了,今天看一段代码没看明白扎回事,才发现这里有个知识点都忘的一干二净了,这里来回顾一下
定义过滤器有两种常见的方式: 双花括号插值和 v-bind 表达式
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
- 1
- 2
- 3
- 4
- 5
那么过滤器有什么用呢?其实一般用来格式文本,比如我们往往拿到的时间是时间戳,那么就可以很简单的通过过滤器来格式成我们常见的格式,这个非常便捷。
使用上的我们先要定义过滤的方法,这个可以全局定义或者组件内定义
//组件内定义
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
//全局定义
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
基本简单的使用这样就够了,还想了解更多可以去看官方文档
十五、知识点:VUE中实现组件通信的方式有几种 ⭐⭐⭐⭐⭐
在VUE中实现通信有很多种的方式,每一种都有其对应的使用情况,下面我们不对每一种做深入的介绍,只要是介绍有多少种方式和他们的区别。
首先我们看看有哪些方式:
- props
- emit
- v-model
- refs
- provide/inject
- eventBus
- vuex/pinia(状态管理工具)
常见的是上面的这几种,少见的其实还可以算上插槽 slot, 混入,路由携带参数,localStorage, $parent / $children;等
1. props
常常使用在父组件传递給子组件通信的过程中,首先在子组件中使用 props,来接收对应的属性,而在父组件中使用子组件的地方,添加上面定义的属性。
2. emit
这个就和上面的相反,是使用在子组件给父组件传递值的中。子组件中声明对应的事件,当子组件触发事件,就会通过 this.$emit(‘事件’ , ‘数据’)传递到,而在父组件中使用子组件的地方,添加上面定义的事件,这个可以获取子组件传来的值。
3. provide/inject
这对组合往往使用在层级比较深的时候,比如A组件下可能还有 B组件 ,B组件下有C组件…E组件.而使用这对 API,就能无论层级有多深都能获取到
4. eventBus
也就是事件总线,简单粗暴,可以到处飞。可以不管你是不是父子关系,通过新建一个Vue事件bus对象,然后通过bus.
e
m
i
t
触发事件,
b
u
s
.
emit触发事件,bus.
emit触发事件,bus.on监听触发的事件。但不建议乱用,不好维护。
5. vuex
对于大型的项目来说往往是很必要的,尤其单页面应用,很多页面嵌套页面,关系很多。而使用 VUEX 就能便捷的统一管理。
面试回答🙋♂️🙋♂️🙋♂️
常见的组件通信方式有通过 props / emit / provide 和 inject / eventBus / vuex 等,一般根据不同的场景来决定使用的方式。比如父子组件通信使用 props ,反过来使用 emit 。而当层级很多的时候使用 provide ,全局的状态管理使用 vuex等。
十六、知识点:Vue事件总线(EventBus)使用介绍 ⭐⭐⭐
简单来说,我们知道Vue中有多种的方式来父子组件传值
- 父传子:props
- 子传父:this.$emit(‘事件’ , ‘数据’)
- 兄弟组件:中间事件总线思想(也叫事件巴士)
- 多层级:父组件中使用 provide ,在子组件中使用 inject
今天我们要来讲的就是事件总线这个,简单来说,它的使用就是首先创建事件总线,也就是我们的大巴
//两种方式,一种是创建一个单独的文件如下面的的bus.js在用到的地方导入,
//还有中就是在项目的main中实例化
// bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// main.js
Vue.prototype.$EventBus = new Vue()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
创建好之后我们就很简单啦
//在需要发送事件的地方用通过 this.$emit(‘事件’ , ‘数据’)来发送
//在需要接收的地方用xxx.$on('事件',(数据)=>{ })来接收
//下面是一个完整的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<Parents></Parents>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
//创建一个媒介来连通两个是组件
const medium = new Vue()
const ABorder = {
template: `我是儿子A
我的名字叫:{{name}}
`,
created () {
medium.$on('letDiscussionName',(val)=>{
alert(val)
})
},
//用props来接收父组件传来的值,同时父组件要绑定属性
props: {
name:{
//设置类型,符合才接收
type: String,
//不传默认为这个
default: '我也不知道呀'
}
},
methods: {
newName() {
this.$emit('newName','大黄狗')
}
}
};
const BBorder = {
template: `我是儿子B
`,
methods: {
discussionName() {
medium.$emit('letDiscussionName','儿子A来讨论')
}
}
}
const Parents = {
data() {
return {
msg: '孩儿们好呀!',
aName: '大花猫',
bName: '大黄狗'
}
},
methods: {
chang(value) {
this.aName = value
}
},
//可以有下面两种形式来调用组件
template: `我是父组件:{{msg}}
`,
components: {
ABorder,
BBorder
}
}
//实例化一个vue,并且绑定到app这个元素
const vm = new Vue({
el: '#app',
components: {
Parents,
}
})
</script>
</body>
</html>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
是不是发现太好用了,特别是当我们的项目中还没有复杂到使用vuex的时候,那么为什么没有被大范围使用呢,这里我们就要注意了。写的时候是非常爽,但是当你修改维护的时候,特别别人看的时候,那得叫苦连天了,使用事件总线的话,就好像一量飞机,载着数据满天飞,多了的话,你就很难维护。那时候真是叫天天不应,叫地地不灵了~~
前些天,公司里的大佬们就有同事强烈认为应该完全禁止使用,哈哈哈,其实工具没错,但不应该滥用。
十七、知识点:简单认识Vue-router 中hash模式和history模式的区别* ⭐
- 最明显的是在显示上,hash模式的URL中会夹杂着#号,而history没有。
- Vue底层对它们的实现方式不同。hash模式是依靠onhashchange事件(监听location.hash的改变),而history模式是主要是依靠的HTML5 history中新增的两个方法,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改。
- 当真正需要通过URL向后端发送HTTP请求的时候,比如常见的用户手动输入URL后回车,或者是刷新(重启)浏览器,这时候history模式需要后端的支持。因为history模式下,前端的URL必须和实际向后端发送请求的URL一致,例如有一个URL是带有- 路径path的(例如www.lindaidai.wang/blogs/id),如果后端没有对这个路径做处理的话,就会返回404错误。所以需要后端增加一个覆盖所有情况的候选资源,一般会配合前端给出的一个404页面
十八、知识点:VUE中 keep-alive 的学习 *⭐⭐⭐
为什么
当我们浏览商品时,我们点击了商品进入了商品的详细页,当我们返回的时候,我们需要的是返回到之前点击前的位置继续,而不是重新到了开头或者刷新了。
那么这里就是需要用到了 keep-alive 。
用 keep-alive 包裹组件时,会缓存不活动的组件实例,也就是我们上面的商品列表,而不是销毁,使得我们返回的时候能重新激活。keep-alive 主要用于保存组件
状态或避免重复创建。避免重复渲染导致的性能问题。
大家可以看这个实例,可以很明显感受到 keep-alive 的作用,他使得我们返回的时候,还能看到之前的内容。
具体的一些实现可以去看看一些文章的详细讲解
常见场景
页面的缓存,如上面的,保存浏览商品页的滚动条位置,筛选信息等
注意❗❗❗
这个 keep-alive 和 这个 Connection: Keep-Alive 是不一样的,这个属性的作用是,往往我们三次握手后,传输完数据就会断开连接进行四次挥手,关闭TCP连接,但是当我们在头信息中加入了该属性,那么TCP会在发送后仍然保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求,保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。
十九、知识点:v-if 和 v-for 为什么不建议一起使用呢? ⭐⭐⭐⭐⭐
先来回顾一下这两个指令是分别做什么的。
v-if:根据表达式的真值来有条件的渲染元素,在切换时元素及它的数据绑定 / 组件被销毁并重建。
v-for:基于源数据多次渲染元素或模板块。此指令之值,必须使用特定语法 alias in expression,为当前遍历的元素提供别名,同时设置 独一无二的 key 。
那是因为 v-for 的优先级比 v-if 高的。
我们有时候会写出下面的例子:
<div id="app">
<p v-if="isShow" v-for="item in items">
{{ item.title }}
</p>
</div>
- 1
- 2
- 3
- 4
- 5
而实际上这段代码会先循环然后再进行判断,每次v-for都会执行v-if,造成不必要的计算,这会带来性能上的浪费。避免这种使用在同一个元素的情况,我们可以在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
<template v-if="isShow">
<p v-for="item in items">
</template>
- 1
- 2
- 3
如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项。
<div>
<div v-for="(user,index) in activeUsers" :key="user.index" >
{{ user.name }}
</div>
</div>
data () {
return {
users,: [{
name: '1',
isShow: true
}, {
name: '2',
isShow: true
},{
name: '3',
isShow: false
}]
}
}
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isShow;//返回isShow=true的项,添加到activeUsers数组
})
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
面试回答🙋♂️🙋♂️🙋♂️
首先,两者的作用分别是 v-if 是根据真值来判断是否渲染组件,而 v-for 是循环渲染组件列表。之所以不能一起使用是因为 v-for 的优先级 比 v-if 高。
这会导致的情况就是,当我们在一个循环列表中做判断时,他会先全部列表渲染出来,然后再根据判断做销毁,这会造成不必要的浪费,就是做好了,多出来的砸坏。一般我们可通过计算属性对数据做过滤,把不必要的数据过滤掉,就不必使用 v-if 。
如果使用v-show就不一样了,v-show 和 v-if 的区别在于v-show 是通过display属性设置为 none 来隐藏属性的,而 v- if 是销毁 DOM上的元素的,所以如果同时使用 v-show 和 v-for 是没有影响的。
二十、知识点:VUE 中 key 的作用? ⭐⭐⭐⭐⭐
关于 key ,我们常常在 v-for 中会接触到,当我们需要进行列表循环的时候,如果没有使用 key ,就会有警报提示。
//场景一:循环列表
<div v-for="num in numbers" :key="index">
{{num}}
</div>
//场景二:通过时间戳强制重新渲染
<div :key="+new Date()" >+new Date()</div>
- 1
- 2
- 3
- 4
- 5
- 6
那么为什么需要 key呢?
因为使用 key 相当于给 vnode 添加上一个唯一的 id,主要是在 DOM DIFF算法中使用的下面我们看个例子:
-
如果上面的场景一
items 的值为 [1,2,3,4,5,6,7,8,9,10] 那么就是渲染出十个div 是吧。如果没有 key ,那么当我们现在 items 的值变为 [0,1,2,3,4,5,6,7,8,9] 呢,那么机会第一个原来是 “1” 的 div内容变为“0”,而原来是“2”的div内容变为“1”…以此类推。(因为没有key属性,Vue无法跟踪每个节点,使用的是“就地复用” 得策略,通过这样的方法来完成变更)
但是如果key的情况下,Vue能跟踪每个节点,就会直接新增一个 div 添加到内容是“1”的div前面。根据key,更准确, 更快的找到对应的vnode节点。
设置key能够大大减少对页面的DOM操作,提高了diff效率,更加的高效更新虚拟DOM
-
还有的用途就是我们的场景二
用来强制替换元素,当key改变时,Vue认为一个新的元素产生了,从而会新插入一个元素来替换掉原有的元素。所以上面的元素会被删除而而重新添加。但是如果没有添加 key的话,就会直接替换div 里面的内容。而不是删除添加元素。
注意点❗️❗️❗️
尽量不要使用索引 index 做 key 的值,要使用唯一的标识值,比如 id 之类的,因为如果使用数组索引 index 为 key,当向数组中指定位置插入一个新元素后,因为这时候会重新更新 index索引,对应着后面的虚拟DOM的 key 值全部更新了,这时候做的更新是没有必要的,就像没有加key一样,因此index虽然能够解决key不冲突的问题,但是并不能解决复用的情况。如果没有唯一ID的情况下,可以使用用组件的uid拼接index的形式,因为uid是唯一的 。或者生成一个时间戳~。
面试回答 🙋🙋🙋
关于在 v-for 中使用 key 是因为使用 key 相当于给节点添加上一个唯一的 id,在 DOM DIFF算法中能更高效的更新虚拟DOM。
当没有使用的时候,如果我们需要对一个循环渲染的列表插入一个,那么实际上会 “就地复用” 的策略,比如我们需要在最前面插入一个,那么就会后面的全部往后变更,发生多次DOM操作,这是因为 Vue无法跟踪每个节点,而使用 key 后,节点标识,定位到最前面,然后插入修改,只发生一次DOM操作,大大优化了性能。
其实这里还得补充上面关于 “使用索引 index 做 key 的值” 的问题,但是我们不要先说,等面试官问到才回答,虽然大家都知道是会准备的,但是我们说的太多就没有留下给别人提问的地方了。好的情况是面试官问你的基本问题,你都能回答,当他再问一下细节,你也能回答,并且还能扩充点细节,这是一个你来我往的过程,不是你一个人的表演。
二十一、知识点:简单认识虚拟DOM⭐⭐⭐
一、什么是虚拟 DOM呢?
再过去或者我们原生JavaScript的时候,我们可以发现,当我们需要改变视图的数据的时候,我们往往需要先获取到这个 DOM 元素,然后对其进行更新。也就是:
数据改变-->操作DOM--> 视图更新
但是我们在 VUE或者 React 中是直接改变 Data 就能实现视图的更新了。
数据改变-->视图更新
那么要是我们每一次数据改变都需要操作 DOM,那就非常麻烦,而且慢。因为 JavaScript执行时很快的,但是操作 DOM就不是了。所以就有了虚拟 DOM了
数据改变-->操作虚拟 DOM(计算变更)-->操作真实的 DOM--> 视图更新
那么什么虚拟 DOM 呢?
可以说虚拟 DOM 本质上是 JS 和 DOM 之间的映射,表现为是一个能描述 DOM结构 及其属性信息的 JS对象。那么下面我们看一个例子为什么这么说吧:
//我们定义的 DOM
<div id="app">
<p class="text">hello</p>
<h1 class="text">hello world!!!</h1>
</div>
//转换为虚拟 DOM
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'hello'
]
},
{
tag: 'h1',
props: {
className: 'text'
},
chidren: [
'hello world!!!'
]
}
]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
我们可以看出,实际上就是通过 JavaScript 对象来作为基础的树,用对象的属性来描述节点,然后通过映射成真实的 DOM 结构。
二、那么这么做有什么好处呢?有两点
1、 优化性能
我们常说要减少重排的发生,那是为什么呢,因为那会涉及到 DOM 树的重新渲染。而操作 DOM 树是比较慢的,且 DOM 元素的数量谁很庞大的,当操作 DOM 很容易带来页面的性能问题。很容易给用户带来不好的体验,而这点是很重要的,可以说我们前端页面是与用户的第一个窗口。
比如,正常情况下,当我们要更新10个节点的时候,浏览器就会计算 10次 ,一次一次的进行更新。而在使用虚拟 DOM ,他相对就好像多了一个缓存区,当 DOM 操作(渲染更新)比较频繁时,它会先将前后两次的虚拟 DOM 树进行对比,定位出具体需要更新的部分,生成一个“补丁集”(也就是一个 js对象),最后只把“补丁”一次性打在需要更新的那部分真实的 DOM 树上,避免了很多没必要的计算,提高了性能。
2、抽象化渲染过程
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。
解耦了视图层和渲染平台,带来了更多的可能性~
有点像我们语言中,都会编译成 AST ,而在这个阶段,可以实现多种转化,就像 Babel ,和 ESList 等。
3、缺点也是有的:在初次渲染的时候,会多出一层 虚拟DOM的计算,而且使用虚拟DOM也不一定是高效的,往往当我们每一次改动不大的时候,才是相对高效的,但是如果我们改动大的话,相比还多出了更多的计算,是不利的。
面试回答🙋♂️🙋♂️🙋♂️
虚拟 DOM 本质上是 JS 和 DOM 之间的映射,表现为一个能描述 DOM结构 及其属性信息的 JS对象。没有使用 虚拟DOM 时,当我们改变数据–>就需要操作DOM–>然后视图更新,当是当我们使用虚拟DOM,就是我们改变数据–>框架就会操作虚拟 DOM进行计算变更–>之后映射到真实的 DOM上–> 完成视图更新。
当我们说虚拟DOM的优点,我们常说 虚拟DOM 提高效率,其实这需要根据场景而言的!当操作 DOM 的数量比较少的时候,使用 虚拟DOM 反而效率低,因为添加了更多的计算等操作。而当操作大量 DOM 的时候,使用 虚拟DOM 会将多次操作合并为一次更新,减少 JavaScript 操作真实 DOM 的带来的性能消耗。另外一个重要的优点是 虚拟 DOM 抽象了原本的渲染过程,实现了跨平台,跨端的能力!不再局限于浏览器!
推荐文章:Vitual DOM 的内部工作原理
二十二、知识点:简单了解 DIFF 算法 ⭐⭐⭐
昨天简单了解了一下虚拟 DOM,那么今天肯定就是和她密不可分的 DIFF算法啦。其实 DIFF算法的作用简单来说就是一句话:同层树节点比较的算法
那么 DIFF算法是如何工作的?
一、首先是先计算新老DOM的最小变化
该算法会先遍历一遍老的DOM,然后在遍历新的DOM,最后会判断是改变/新增/删除来重新排序。
这样无疑是非常耗费计算的,我们看一看出总共遍历了三回,如果有一千个节点,那么就湖发生了十亿次的计算。
二、diff算法的优化
diff 算法的优化,也就是这个算法的核心部分了,简单来说就是针对具有相同父节点的同层新旧子节点进行比较,不相同的话就会新增或者删除,而不是使用逐层搜索递归遍历的方式。时间复杂度为O(n)。
-
只比较同一层级,不跨级比较
-
标签不同,直接删除,不继续比较
-
标签名相同,key相同,就认为是相同节点,不继续深度比较
-
看个实例:

- 第一次比较:根节点都是 a ,所以会对其子节点进行比较
- 第二次比较:由于新的 DOM树只有 c节点,所以会把 b节点以及其下的子节点都全部删除。
- 第三次比较:由于都拥有 c节点,所以会比较其子节点,发现新的 DOM树有 e节点,所以会新添加 节点e到c节点下方。
到这里就算结束啦,从源码层面的分析我就不班门弄斧了~~
推荐文章:
知识点、 SPA单页面应用 *⭐⭐⭐
知识点、 data 为什么是一个函数 * ⭐⭐⭐⭐⭐
vue中的data是一个对象类型,对象类型的数据是按引用传值的,这就会导致所有组件的实例都共享同一份数据,这是不对的,我们要的是每个组件实例都是独立的
浏览器原理
一、知识点:从输入URL到页面展示这中间发生了什么⭐⭐⭐⭐⭐
这个是个非常重要的点了,而且涉及到的知识点很多,可扩展性很大,想一棵树的大主干,可以有很多的分支,而认证去分析里面的每一步,可以很好的体系化连接每一个知识。

接下来我们就慢慢分析一波:
执行过程:
- 用户输入
- 浏览器进程判断是否符合 URL 规则,符合的会加上协议等,进行访问,不符合的会使用默认浏览器引擎搜索,组装为完整的 URL
- 浏览器进程把 URL请求 通过进程通信发送给网络进程。
- 网络进程检测是否有缓存
- 是否有缓存:有缓存就会判断是否过期,一般通过 Expires 和 Cache-Control 这两个字段判断是否过期,没有缓存就会直接下一步。
- 有缓存就会进行下面操作:强缓存 和 协商缓存,先进行强缓存,强缓存不会向服务器请求验证,通过 Expires 和 Cache-Control 这两个字段判断缓存是否过期话有没有效,如果过期了就会向服务器验证,这里之后就是协商缓存,通过协商缓存,服务器会告诉浏览器缓存还是否有效,是否需要更新,如果不需要更新就会返回304,告诉浏览器直接使用,反之返回200,告知下浏览器使用返回的响应信息。
- 在发送请求前就会查找是否存在主机 IP地址,这里会到浏览器缓存,本机缓存,hosts文件,路由器缓存,isp DNS缓存中查找
- 如果也还是没有就会进行 DNS域名解析,浏览器解析 URL 获取到协议,主机名,端口,path等,接着会组装一个 HTTP 请求报文(带解析的域名会放在这个 DNS 请求报文里,会使用 UDP )来获取服务器的 ip地址
- 接着判断是否为 HTTPS ,是的话会先建立 TLS 连接
- 不是 HTTPS 或者建立 TLS 连接之后就会与服务器通过三次握手建立 TCP连接发送 HTTP请求
- 服务器接收解析请求头的信息,将请求转发到服务程序
- 服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
- 处理程序读取完整请求并准备HTTP响应
- 服务器将响应报文通过TCP连接发送回浏览器
- 浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,通过四次挥手关闭TCP连接。
- 网络进程解析响应信息,检测状态码,如果是2XX,就会继续处理请求,如果是其他就会根据响应码做相应的处理。
- 如果为2XX,就会检测响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
- 接着到浏览器分配渲染进程,检测当前的 URL 是否和之前打开的渲染进程的根域名相同,是的话会复用进程,不是的话会重新创建个新的渲染进程。
- 渲染进程准备好后,就到了提交 “文档”。
提交文档的文档是指网络进程返回的响应数据
“提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。 - 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。 - 这时候完成页面的显示了
面试回答🙋♂️🙋♂️🙋♂️
面试总不能这样哔哩吧啦的说一堆出来吧,这样会显得你明显是背的,虽然也是,但是不要这么明显,面试的时候我们回答大概,这样给面试官留下提问的空间,有来有回,也便于我们表现我们知道的东西,代入我们的主场:

深化文章:深入了解输入网址到展示网站发生什么
二、知识点:浏览器渲染过程 ⭐⭐⭐⭐⭐
一、页面生成的过程
浏览器渲染的过程主要有下面几步:
- 通过 HTML解析器,解析 HTML 解析成 DOM 树。
- 通过 CSS解析器,解析 CSS文件构成层叠样式表模型 CSSOM 树。
- 结合 DOM树 和 CSSOM树 合并为渲染树(rendering tree)
- 布局,浏览器在屏幕中画出渲染树的每一个节点,被称之为布局( Layout )
- 绘制,将渲染树的各个节点绘制到屏幕上,这一步被称为绘制( Paint )

三 、知识点:浏览器缓存 * ⭐⭐⭐⭐⭐
一、什么是缓存
浏览器缓存就是把我们访问过一次的 Web资源(html页面,图片,js,数据等) 保存到本地,当我们再一次访问相同 URL 的时候,就会根据缓存机制的规则判断是否使用之前的 WEB资源,还是向服务端发起请求获取。
二、为什么使用缓存呢?
其实我们所有的都系都是守恒的,要么使用空间换取时间,要么就是使用时间来换空间。没有两全其美的。
首先有如下优先:
- 使用缓存减少了数据的传输,减低了我们等待的时间,提升了用户的体验
- 降低对服务器的要求,减少对服务器的访问,节约通信流量和通信时间
- 降低了时延,因为当我们服务器和客户端距离远时,页面的加载会变慢,而使用缓存可以更快的
可以说缓存就是用 内存换时间
三、 缓存过程:
- 第一次请求资源时:
- 因为是没有缓存的,向服务器发送请求
- 服务器返回资源,如果资源可以缓存,响应头中会包含资源的缓存参数(是否缓存,缓存时间等消息);
- 第二次请求时:
- 因为第一次缓存了资源,所以浏览器会先判断缓存是否过期(Cache-Control 和 Expires),是否命中强缓存
- 没过期就会直接使用缓存内容(也就是击中强缓存)
- 否则就把请求参数加到请求头中传给服务器,服务器返回是否资源过期(Last-modified 和 Etag),没过期会返回304(也就是击中协商缓存),告诉客户端直接使用缓存。
- 要是过期了就会返回200,也就是资源过期,使用浏览器返回的资源。


上面的过程就带来了下面的两个问题了!!
四、两个关键问题 *
问题一、如何判断资源是否过期呢?(强缓存)
想了下还是吧这点的标题强缓存换为现在这个,因为思考来思考去,强缓存是指这个过程的名词,这个问题才是下面知识点的解释。其实要是介绍强缓存,也就是讲述这个过程~
那么如何判断资源是否过期呢?
判断是否过期主要是:
缓存更新周期 + 缓存最后修改时间 是否大于 现在的时间

下面我们看看这里面的每个属性的作用:
Expires属性
在早期是使用 Expires 的,该属性为服务端返回的过期时间,是一个到期的时间戳,客户端第一次请求服务器,服务器会返回资源的过期时间。如果客户端再次请求服务器,会把请求时间与过期时间做比较。但是 Expires 具有一个缺点,返回的是服务端的过期时间,也即是要求客户端和服务端的时间严格同步,所以当浏览器和服务的时区跨度比较大的时候,就会有误差,Expires 是 HTTP1.0 的属性,所以在 HTTP1.1 中使用 Cache-Control 替代,当两者都存在的时候,Cache-Control 的优先级会更高。
注意 ❗️❗️❗️
Expires 和 Cache-Control 的区别:
Cache-Control
1、缓存更新周期
Cache-Control ,该属性又多个属性值,其中我们最常见的是使用 Cache-Control : max-age 来表示缓存支持存在的时间,下面我们看一看常见的属性值:
- Cache-control: no-cache 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。
- Cache-control: no-store 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
- Cache-control: public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。
- Cache-control: private 表明响应只能被单个用户缓存,不能作为共享缓存
- Cache-Control: max-age= 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
- Cache-control: s-maxage=
2、缓存最后修改时间
上面我们有了 “保质期”了,下面我们还需要的是生产日期。关于这个有两个参数 Last-Modified 和 Etag。
-
Last-Modified :标识该资源的最后修改时间,web服务器在响应请求时,web服务器在响应请求时,告诉浏览器资源的最后修改时间
-
Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识
注意 ❗️❗️❗️
Last-modified 已经足够浏览器知道本地的缓存副本是否新鲜,为什么还需要 Etag?
问题二、如何判断本地缓存是否和服务端的相同 ?(协商缓存)
上面我们了解了如何解决第一个问题,也就是强缓存,判断资源是否过期,那么第二个问题,也就是协商缓存,判断本地缓存是否和服务端的相同是如何解决的呢?
我们可以知道 强缓存,给资源添加了一个过期时间,客户端通过判断是否过期来决定是否直接使用缓存还是向服务端发起请求。
但是当超过这个过期时间时,浏览器保存的资源也有可能还是和服务端相同的,也就是不需要更新,所以接下的步骤就是客户端向服务端发起请求,询问资源过期了是否还有效?有效的话,服务端就会返回 304 告诉客户端可以继续使用,更新缓存参数。要是无效了,就返回200,告诉客户端使用最新返回的资源,这个过程就是 协商缓存。
那么如何判断是否有效呢?
- 当使用expires字段的时候,请求时再带上expires字段
- 当使用的是last-modified字段的时候,请求时带上If-Modified-Since,表示请求时间
- 当使用的是Etag字段的时候,请求时带上If-None-Match
Last-modified / If-Modified-Since
- Last-modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间
- If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上If-Modified-Since,表示请求时间,web服务器收到请求后发现有头If-Modified-Since,则与被请求资源的最后修改时间比较。若最后修改时间较新,说明资源已经有变动,则响应资源,HTTP200;如果最后修改时间较久,那么资源没更新,使用缓存,返回HTTP304.
Etag / If-None-Match
- Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器来定),默认有对文件的索引节(INode),大小(Size),和最后修改时间(MTime)进行hash后得到的
- If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
🚩🚩🚩
强缓存是判断缓存资源是否过期,协商缓存是判断缓存资源是否和服务器的相同,也就是是否需要更新
五、缓存的特点
- 首先HTTP缓存都是从第二次请求开始的。
- HTTP缓存分为强缓存和协议缓存
- 强缓存命中的话不会发请求到服务器,协商缓存一定会发送请求到服务器。
- 缓存可以是在缓存服务器也可以在客户端浏览器中
- Last-Modifed/If-Modified-Since的时间精度是秒,而Etag可以更精确,是根据时间戳的
- Etag优先级是高于Last-Modifed的,所以服务器会优先验证Etag
- Last-Modifed/If-Modified-Since是HTTP1.0的头字段,而Etag是属于HTTP 1.1属性
六、缓存的位置
面试回答🙋🙋🙋
浏览器缓存就是把我们第一次访问的 WEB资源保存到本地中,当我们再一次访问相同的 URl的时候就会根据缓存规则决定是使用缓存还是向服务端请求。
一般判断使用缓存有两个阶段,先是强缓存,判断缓存是否过期,如果没有过期就会直接使用,判断过期一般通过 Expires , Cache-Control ( Etag, Last-Modifed )这几个属性判断。
当判断是过期后,就会进入到协商缓存的阶段,就是判断缓存是否还和服务端的相同,一般通过根据强缓存使用的判断属性来选择,有Expires , If-Modified-Since(Last-Modifed) , If-None-Match(Etag) 来判断当前客户端缓存是否和服务端相同。相同就会返回 304 状态码,告诉浏览器使用缓存,否则就会返回200,告诉浏览器使用最新的响应信息。
注意 ❗️❗️❗️
一般答完之后,面试官还会询问一些细节,比如具体如何判断缓存过期 和 如何判断缓存相同,以及其中为什么有了 XXX属性就够了,但需要另一个!
四、知识点:关于一段代码执行前的“编译” ⭐⭐⭐
一、编译语言和解释语言
我们写的代码一般不能被电脑直接识别,所以就有了编译器和解释器,也对应不同的语言
编译语言一般是 C / C++, Go等,这类语言首次执行会通过编译器编译出机器能读懂的二进制文件,每次运行的时候会直接运行这个二进制文件。
解释语言一般是Python,JavaScript等,每次执行都需要解释器进行动态解释和执行。
二、编译的过程
那么编译一般来说有六个步骤
- 分词 / 词法分析
a. 把字符组成的字符串分解成有意义的代码块(词法单元), 如 var a = 2,会分解为 var , a, =,2; - 解析 / 语法分析
a. 将上面的词法单元流(数组)转换为由元素逐级嵌套所组成的代表程序语法结构的树,也就是AST - 生成抽象语法树(AST)
- 词义分析
- 生成二进制文件或者字节码
- 执行
编译语言和解释语言的主要区别在于词义分析后生成的类型不同,他们都会生成AST这一步,3,4,5,6可以合并称之为代码生成
我们前端开发用的JavaScript就是解释型语言,其实一开始是没有字节码的,是直接将AST编译成机器码,所以效率是很高的,但是机器码占用的内存过大,所以又有了字节码的出现。这里又涉及到一门新的技术 JIT (即使编译)的出现。
所以我们的V8引擎使用的是 字节码 + JIT 的技术
十二、知识点:AST 抽象语法树 ⭐⭐⭐
首先我们知道我们输入一段 JavaScript 代码,他会先经过词法和语法分析得到 AST 也就是我们的抽象语法树。那么为什么需要转化为 AST 呢?那是我们编写的的的无论是编译型语言还是解释型语言,都是不能被编译器或解释器所理解的。而且转化为 AST 还能有很多的便利,我们的 babel , ESlint 很多工具都是在这一步进行的。
那么我们看看 AST 抽象语法树是如何生成的吧~
1. 首先第一步是分词,也就是词法分析
分词就是把一行行代码拆分为每一个 token ,也就是 语法上不可再分,最小的单位。同时去除空格,对 token 分类等。
// 源码
let aaa = '12: 30'
// Tokens数组
[
{ type: { ... }, value: "let", start: 0, end: 3, loc: { ... } },
{ type: { ... }, value: "aaa", start: 4, end: 7, loc: { ... } },
{ type: { ... }, value: "=", start: 8, end: 9, loc: { ... } },
{ type: { ... }, value: "12:30", start: 10, end: 15, loc: { ... } },
]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
上面的代码就会拆分为,其中关键字“let”、标识符“aaa” 、赋值运算符“=”、字符串“12: 30”四个都是 token,而且它们代表的属性还不一样。添加到 Token 数组中。
2. 词法分析之后就进行解析,也就是语法分析
其作用是将上一步生成的 token 数据,根据语法规则(扫描 token 列表,形成语法二叉树)转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
通过这样就构建起 AST 抽象语法树。(好吧,也就只能照搬被人想法说说,这个还不能理解,以后来一探究竟吧~)
四、知识点:LHS 和 RHS ⭐
首先变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(之前没有声明),然后运行时引擎会在该作用域中查找该变量,如果能找到就会对他赋值
那么引擎查找中就涉及到我们这里要讲到的 LHS查询 和 RHS查询
这里我们不能简单的认为是左右来判断,而是根据:
LHS:赋值操作的目标是谁
RHS:谁是赋值操作的源头
也可以说查找的目的是对变量进行赋值,那么就会使用LHS 查询;如果目的是获取变量的值,就会使用RHS 查询
考虑以下代码:
console.log(a);
其中对a 的引用是一个RHS 引用,因为这里a 并没有赋予任何值。相应地,需要查找并取得a 的值,这样才能将值传递给console.log(…)
相比之下,例如:
a = 2;
这里对a 的引用则是LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为=2 这个赋值操作找到一个目标。
看个例子
function foo(a) { //LhS
console.log(a) //RHS
}
foo(2) //RHS
这里面就涉及到两种查询了
同时 LHS 和 RHS 会现在当前的作用域查询,没有找到的话就会到上一层查找,最后到全局作用域
五、知识点:浏览器内核和浏览器的作用 ⭐⭐⭐
一、 浏览器的组成
浏览器的组成,主要分为两部分 外壳+内核。
外壳指菜单、工具栏等,主要是为用户界面操作、参数设置等提供的。它调用内核来实现各种功能。
内核是浏览器的核心,内核是基于标记语言显示内容的程序或模块。(我们可以写插件的,就是能对外壳进行定义,以及调用一些内核API,感觉挺好玩的,还没试过~~~)
二、浏览器作用
向服务器发出请求,在浏览器窗口中展示您选择的网络资源。这里所说的资源一般是指 HTML 文档,也可以是 PDF、图片或其他的类型。资源的位置由用户使用 URI指定,浏览器根据HTML规范进行解释。
三、浏览器内核
浏览器的核心部分是“渲染引擎”也会简称“浏览器内核,负责解释页面语法(HTML、CSS 解析、页面布局)和渲染(显示)页面。但是现在一般我们提到的大部分“浏览器内核”都包含了 JavaScript 引擎,用来处理一些动作,动态效果,所以我们可以一般认为浏览器内核包含渲染引擎和JavaScript引擎。因为浏览器的引擎不同,对我们的网页语法的解析就会产生一些不同。所以我们写CSS的时候,一般会对全局进行一些初始化,以及我们需要对页面做兼容性处理。
四、浏览器使用的内核分类
- Trident 内核:IE、MaxThon、TT、The World、360、搜狗浏览器等(当年的大哥了,没落后,IE都被淘汰了,不过国内一些老的机构的页面还是基于IE的,比如教师资格考试就要在IE上报名)
- Gecko 内核:Netscape6 及以上、FF、MozillaSuite/SeaMonkey 等(什么鬼东西,要不是搜了,都没听过)
- Presto 内核:Opera7 及以上
- Webkit 内核:Safari、Chrome 等(大哥大了~~~edga用了后,明显用户增加了)
七、知识点:DOM 事件流 * ⭐⭐⭐
一、什么是事件流:
我们点击一个按键,那么他是如何传递的呢,是从文档顶部一层层传入到这个按键,还是从这个按键传出去呢?
- 曾经在 IE 就是从里面往外(确定的逐步到不确定的),叫做事件冒泡
- 而到了 Netscapte 就是从外面往里(不确定的逐步到确定的),叫做事件捕捉
- 最终在 W3C 的定义规范中,就把两者都包含了~~~(哈哈哈,谁都赢了,但谁也没赢)
所以现在在 DOM事件模型中会分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
- 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
那么为什么会有事件捕获和事件冒泡呢 这就涉及到事件委托
那么什么是事件委托呢,事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件
这里里面涉及的捕获阶段,冒泡阶段,事件委托建议好好看一些代码的实现~
参考文章
https://juejin.cn/post/6844903781969166349#heading-5
https://www.jianshu.com/p/6512139d1d9e
八、知识点:defer 和 async ⭐⭐⭐⭐⭐
一、为什么
往往我们在网页开发中,常常会引入 script 标签,同时我们知道,在 Http 网页中会按顺序之上往下的执行,这也就是为什么我们需要把 mate 标签放在最上面,而把 script 放在最下面,前者是便于网络爬虫爬取,便于优化网站的排名,后者是避免脚本加载的过程造成的阻塞,影响网页的渲染.,针对这个问题,所以就提出了两种解决的方案 defer 和 async,在了解这两个解决方案前我们先看看。
二、页面的加载和渲染过程是如何的
- 首先浏览器发送 HTTP请求获取到HTTP文档,获取到后会自上往下顺序执行,构建DOM
- 构建中,如果遇到外联声明或者脚本声明,会暂停文本构建,创建新的网络请求,获取外联样式或者脚本文件
- 获取到后,会执行外联样式文件或脚本,之后才会继续文档解析
- 最后完成文档解析后,将 DOM 和 CSSDOM 进行关联和映射,最后将视图渲染到浏览器窗口
所以说这个过程中,外联文件的下载执行和文档的构建是同步进行的,如果当我们外联的文件遇到问题,一直没有下载下来,而同时文档的构建也没法进行,就会很大程度上影响用户的体验,也就是阻塞了文档的的解析。所以我们看下面的办法是如何解决的。
三、defer
这个属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded 事件前执行。带有该属性会告诉浏览器马上下载该脚本,但是并不会马上执行脚本,而是在文档解析完成后才进行,同时对于多个脚本的话,发送的网络请求是同步请求的。
四、async
带有该属性,会告诉浏览器马上下载该脚本,同时继续文档解析,当下载完成后会马上解析执行脚本。而遇到多个脚本的时候,发送的网络请求是异步的。 所以可能会出现后面的脚本比前面的脚本更加前进行解析执行。所以如果使用该属性,要注意脚本之间不存在依赖关系。
五、defer 和 async 的区别
script标签 js执行顺序 是否阻塞解析HTTP 网络请求 script 在HTTP中的执行顺序 阻塞 网络请求同步 script defer 在HTTP中的执行顺序 不阻塞 网络请求同步 script async 网络请求的返回顺序 可能阻塞也可能不阻塞 网络请求异步 
所以基于上面两者的有特点,如果你的脚本依赖于文档内容是否解析完,以及脚本之间是否依赖,那么使用 defer,反之就是async。
补充: 一般执行过程
文档解析->脚本加载->脚本执行->DOMContentLoadeed
面试回答🙋♂️🙋♂️🙋♂️
关于 这两个 defer 和 async ,往往是在我们引入外部脚本的时候使用的。为什么需要呢?应为我们的浏览器渲染执行是自上而下的,当遇到外部脚本的时候,就会去发起请求获取脚本,执行完后才会继续往下执行文档解析,这就导致可能出现阻塞的问题,带来不好的体验。所以为了解决这个问题,出现 defer 和 async 。
defer 往往是用在脚本之间不会相互依赖,且页面的解析不依赖于脚本的,因为当遇到添加了 defer 的脚本,浏览器会发起网络请求,然后继续执行文档解析,直到完成后才会执行脚本,并且脚本之间的网络请求是同步请求的。
而 async 的话,当遇到添加了 async 的脚本,浏览器会发起请求,继续执行文档解析,当脚本获取到脚本后,就会停止文档解析执行脚本,并且脚本之间的网络请求是异步的,也就是后面发起的请求可能会比先发起的请求快执行。
九、知识点:window.onload 和 DOMContentLoaded的区别 ⭐
一、window.onload和DOMContentLoaded的区别
他们的区别是按照执行顺序来的:
DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。onload 事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。
二、我们再来看一下DOM的执行顺序:
- 解析HTML结构。
- 加载外部脚本和样式表文件。(这里还有之前介绍过对于脚本使用aysc和defer的区别,可以到上一期看啊看)
- 解析并执行脚本代码。//js之类的
- DOM树构建完成。//DOMContentLoaded
- 加载图片等外部文件。
- 页面加载完毕。//onload
我们可以看到在第4步的时候DOMContentLoaded事件会被触发。在第6步的时候onload事件会被触发。那么为什么需要分为两个阶段呢?
因为当我们需要给元素添加某些触发方法时,但是这时元素还没有渲染出来,那么是没有效果的,而这两个回调就能避免这些情况。同理的我们在vue中也会遇到这种问题,这时候使用的是一个叫 $nextTick 的函数。
十、知识点:简单请求和复杂请求 ⭐⭐⭐⭐⭐
一、跨源资源共享(CORS)
在说简单请求前我们先来了解一下我们往往在学习前端之初和后端对接接口的时候遇到的一个常见的问题,那就是跨域!
那么了解啥是跨域之前,我们看看一什么是同源?因为只有当不是 同源(SOP) Same Origin Policy 的时候,才会涉及到 跨域 (CORS)Cross-Origin Resource Sharing 了!
二、同源的MDN定义:
如果两个 URL 的 protocol、port(en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。
也就是当两个 URL 的协议,端口, 主机相同的时候,他们就是同源!
作用就是:不允许不同的ip、端口、协议的应用在浏览器内进行互相资源共享、请求调用。避免出现一些安全问题!也就是你的是网站A,你无法向网站B发送请求。
出于安全性,浏览器限制脚本内发起的跨源HTTP请求。也就是来自其他域的请求,这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求的HTTP资源,除非预检请求的响应报文包含了正确CORS响应头,也就是我们的CORS。
三、如果没有同源策略会如何?
如果没有同源策略的限制,那么如果你打开网站A,且不小心打开病毒网站B,那么病毒网站B,就可以向网站A发送请求,获取到你的在网站A的信息和资料等。现在我们往往试试前后端的分离开发,我们前端获取数据的 API 接口,往往就是跨域请求。如果不使用 CORS 是不被允许的。
四、那么CORS是如何工作的呢?
而使用CORS,就会允许并且能保证安全的发送跨域请求,跨源资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
-
预检请求
-
对那些可能对服务器数据产生副作用的 HTTP 请求方法
-
浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。
-
服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies和 HTTP 认证相关数据)
-
具体流程:
-
网页发起请求
-
根据请求是复杂请求还是简单请求,复杂请求浏览器会先进行预检请求
-
根据预检请求获知服务端是否允许跨域请求,允许接收跨域请求的网站地址。
-
获知服务器返回的结果后,才会真正的发送请求。
那么我们要讲的简单请求和复杂请求所需要了解的基本知识就简单介绍完了!
五、简单请求
某些请求不会触发 CORS 预检请求(发送 OPTIONS 方法),本文称这样的请求为“简单请求
- 使用下列方法之一:GET、POST、HEAD。
- 不得人为设置该集合之外的其他首部字段。该集合为:Accept,Accept-Language,Content-Language,Content-Type
- Content-Type 的值仅限于下列三者之一:text/plain,multipart/form-data,application/x-www-form-urlencoded
- 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
- 请求中没有使用 ReadableStream 对象
六、复杂请求
不是简单请求的都是复杂请求,也可以看做在实际进行请求之前,需要发起预检请求的请求
七、关于预检请求
在发生跨域的时候,只要不满足简单请求的要求,浏览器在发送真实请求之前就会先发送预检请求(OPTIONS请求),向服务端验证是否允许跨域请求。
关于这个预检请求 一般有三个参数比较重要:Access-Control-Request-Method 和 Access-Control-Request-Headers,以及一个 Origin 首部。
举个例子,一个客户端可能会在实际发送一个 DELETE 请求之前,先向服务器发起一个预检请求,用于询问是否可以向服务器发起一个 DELETE 请求:
OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org
如果服务器允许,那么服务器就会响应这个预检请求。并且其响应首部 Access-Control-Allow-Methods 会将 DELETE 包含在其中:
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400
十一、知识点:前端解决跨域的几种方法 ⭐⭐⭐⭐⭐
关于跨域上面有介绍到,这里总结下面两点:
- 为了安全,浏览器不能执行其他网站的脚本,由浏览器的同源策略导致的
- 同源:域名,协议,端口都相等
那么实现跨域请求有哪些方式呢?
一、jsonp
这种方式,实际我们只需要了解一下可以这样处理就好了,实际中并不怎么使用:
- ajax请求受同源策略影响,不允许进行跨域请求,而script标签src属性中的链接却可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。
- 动态生成Script,通过src属性加载
- 缺点:不支持Post,实际不怎么使用
二、中间服务器代理
这种往往我们需要写一个配置文件,配置vue.config.js文件,在里面思想是创建虚拟的本地服务器,当发起请求时,axios向本地服务器请求,本地服务器再向目的服务器请求,这样就不存在跨域问题了。
前端部署的地址->中间服务器->目标服务器,如同下面的请求端口:
127.0.0.1:8000->127.0.0.1:8000->127.0.0.1:8888
具体实现如图:

三、CORS跨域资源共享
- 服务器端进行配置,加一个响应头,这种是最常见的方式,而且是由后端负责,前端不用做处理
面试回答
什么是跨域呢,那是应为,为了安全起见,我们的浏览器有一个同源的策略,不允许非同源请求,那么什么是同源,就是协议 / 端口 / 主机都是一样的,这也叫元组。如果非同源的话,可能出现安全问题 CORS 也就是别人获取到你的凭证比如才cookie ,token等,然后冒充你向网站发起请求(比如别人拿着你的身份证去银行去你的钱这样~~)。
对于跨域问题我们在前后端的开发中也是很常见的,解决的办法有三种,jsonp 和 中间代理 ,以及后端配置跨域许可。工作中往往是后端配置跨域处理,前端不需要进行处理,触发跨域请求的,往往是一个复杂请求,浏览器会先发送一个预请求,获取到服务器支持跨域的地址等信息。在通过之后才会发起正式请求。而中间代理的话,我们可以在 vue.config.js 文件中设置代理服务器。 jsonp的方式就是通过动态脚本实现。
六、知识点:CSRF和XSS攻击以及如何预防 * ⭐⭐⭐⭐⭐
在 Web 安全领域中,XSS 和 CSRF 是最常见的攻击方式
一、XSS
XSS,即 Cross Site Script,译做跨站脚本攻击,XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。哪些部分会引起XSS攻击?简单来说,任何可以输入的地方都有可能引起,包括URL!
一般有反射型(js链接),存储型(服务器),基于Dom(浏览器)三类方式(自己百度去了解详细哈)
防范方式:
- HttpOnly 防止劫取 Cookie 、
- 输入检查,对于用户的任何输入要进行检查、过滤和转义。建立可信任的字符和 HTML 标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。
- 在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击
二、CSRF
CSRF,即 Cross Site Request Forgery,译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
防范方式:
(比如 Cookie是我们的身份身份证,而Cookie对于任何请求都可以认为是本人,而坏人盗用了你的Cookie在你不知道的情况下,去把你银行的钱去了,在银行看起来也会是你本人取的,而你还不知道~~)
- 验证码:CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击
- Referer Check:根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的”源”。
- 添加 token 验证:CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 Cookie 中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 Cookie 来通过安全验证
三、面试回答
常见的网络攻击有两种,XSS(跨站脚本攻击)和 CSRF (跨站请求伪造)。
对于跨站脚本攻击,就是攻击者想尽一切办法将可以执行的代码注入到网页中。一般有存储型 / 反射型 / DOM型,跨站脚本攻击的共同点就是往浏览器注入恶意脚本,所以要阻止XSS攻击,我们可以通过阻止恶意 JavaScript 脚本的注入和发送。
对于跨站请求伪造,就是攻击者诱导受害者进入第三方网站,获取受害人的凭证,然后冒充受害者对被攻击的网站执行某项操作的目的。跨站请求伪造通常从第三方网站发起,所以使用同源策略保护。使用验证码或者token验证,两者的原理都是前端页面和后端验证,这样即使cookie泄露,没有经过前端验证也无法通过。
十三、知识点:服务端渲染(SSR)和客户端渲染(CSR)⭐⭐⭐
一、服务端渲染(SSR)和客户端渲染(CSR)
之前在和舍友聊天的时候突然了解到这个知识。简单来说就是我们的 HTML文件的完整拼接是在服务端完成返回给客户端,还是在客户端完成,根据不同分为服务端渲染和客户端渲染。
这里为什么出现 HTML文件的拼接呢?其实在早期,我们网页是比较简单的,服务端直接返回完整的 HTML文件给浏览器,浏览器解析展示就完成了。但是随着页面越来越复杂,以及前后端分离的思想。渐渐的服务端不再是返回完整的 HTML文件,而是通过提供 API 来让前端获取数据,前端拿到数据之后再进行拼接完整的 HTML文件。使得前后端可以各自专注负责各自的部分,前端专注 UI 的开发,后端专注与逻辑开发。
所以他们的 核心也就在于谁完成 HTML的拼接,通过这点也能区分是服务端渲染还是客户端渲染。
比如:网页发生了整体的刷新(不是第一次打开),且网络地址发生改变,就是服务端渲染,而如果是局部刷新,且网络地址没有发生改变(也就是ajax),就是客户端渲染。比如我们看当前文章的评论,我们点击下一页,我们可以发现评论内容发生改变了,这个就是一个服务端渲染了。而如果你点击其他的专栏或者首页,上方的网址也发生了改变了,就能看出是服务端渲染了,还有我们收到的邮件网页也一般是服务端渲染。同时也还可以通过判断控制台中的网络请求,看是否有请求接口获取数据。
二、那么这两种渲染方式有什么优缺点呢?
1. 服务器端渲染的优缺点
优点
- 便于SEO,完整的HTML文件便于搜索引擎爬虫。
- 浏览器只需要向服务端请求一次。
缺点
- 数据量大的话会对服务端造成负担 ,因为服务端还得解析HTML文件,而客户端渲染就将这部分的压力分摊到浏览器。
- 不利于前后端分离,提高开发效率。往往需要前端完成一个静态的HTML文件,后端再改为模板来修改(那会做邮件的这个模板的时候就是需要这样,但是是个很简单的页面,如果复杂的话,真的很麻烦)
2. 客户端渲染的优缺点
优点
- 前后端分离,便于开发
- 便于页面信息的展示,而不用每次的网页刷新,而是局部的变动。
缺点
- 便于页面信息的展示,而不用每次的网页刷新,而是局部的变动。
- 不利于SEO(在一些官网等页面,SEO是很重要的,因为这绝对被用户看到的顺序)
三、选择服务端渲染还是客户端渲染呢?
根据业务场景哈,没有最好的,只有最适合的。类似官网这些需要SEO的,又或者本身没有过多的交互的。使用服务端渲染是比较合适的。而像类似后台管理的,有很强的交互,就是客户端渲染比较好。
但是没有绝对的,有些时候往往两者一起使用。
面试回答🙋♂️🙋♂️🙋♂️
服务端渲染 和 客户端渲染 的主要区别在于谁完成 HTML 的拼接,服务端渲染的话,是在服务器上完成,直接返回完整的 HTML 页面,浏览器直接渲染完成。而客户端渲染就是在用户浏览器上完成拼接后,再渲染完成的,比如我们常常看的网页的评论板块。
一般我们是在官网这些地方使用 服务端渲染,因为服务端渲染是便于 SEO,且只会请求一次。但像一些后台管理,或者比较多交互的地方,我们是使用客户端渲染,这便于前后端开发,便于页面的数据展示,不用全局的刷新。
十五、知识点:为什么操作 DOM 慢 ⭐
我们常常说要尽量少去操作DOM,这是为什么呢?
这是因为我们的执行和渲染是分离的!两个互相独立的模块发生交互,就会导致比较大的性能损耗
let DIV = document.createElement('div')
document.body.append(DIV)
DIV.innerTEXT = 'h1'
- 1
- 2
- 3
上面的这段代码执行:
- 首先JavaScript引擎执行(JavaScript引擎线程),也就是所有对 DIV 的操作
- 浏览器发现Body有新的改动,通知渲染线程
- 接着就到渲染线程操作,在页面中渲染 DIV元素
一、知识点:为什么 JavaScript 是单线程 ⭐⭐⭐⭐⭐
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言,这是由其最初的用途来决定的:
与浏览器交互
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
另外个原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript 为一种单线程语言。( 尽管 Worker API 可以实现多线程,但是JavaScript本身始终是单线程的。)
但是,单线程很多时候会造成资源的浪费,JavaScript 如何解决的呢?这个就涉及到:对列和事件循环,同步任务和异步任务机制,以及回调函数等
注意 ❗❗❗
- 在浏览器环境中,有JavaScript 引擎线程和渲染线程等很多线程。我们常说的 JavaScript 是单线程指的是 JavaScript 引擎线程
- Node环境中,只有JS 线程。
二、知识点:浏览器中的事件执行和循环 ⭐⭐⭐⭐⭐
事件循环并不会直接调用函数,而是编排消息执行的时间和顺序,理解这点我们下去看他是如何编排的吧!
一、首先我们看看在 JavaScript 中,任务是如何执行的。
- 首先我们的 JavaScript 是有一个执行栈,执行栈的任务由主线程执行,并且所有 JavaScript 代码都在里面执行。
- 执行过程是按顺序的,也就是同步,但是当遇到异步任务,就会把这个异步任务交给主线程以外的进程或者线程(幕后线程)来执行。
- 当这个异步任务完成后,就会添加到任务队列中等待执行。
- 当主线程完成执行栈中的所有任务后,它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。
- 任务队列里面的任务也叫宏任务,每个宏任务中都包含了一个微任务队列,当这个宏任务完成后,就会去执行当前宏任务中的微任务。
- 如果没有,它就会一直循环等待任务到来。

二、根据上面的执行过程,我们来看看这里面都涉及到什么,他们的作用:
那么上面我们出现了一下名称,下面我们来说说他们每一个的作用:
1、执行栈( Stack )
我们知道 JavaScript 是一个单线程,所以只有一个线程,称之为主线程。所有的JavaScript 代码都会进入执行栈由主线程执行,主线程执行执行栈里的代码中代码就会继续往下,而遇到异步代码就会交到幕后线程执行,待执行栈任务执行完毕之后(也就是同步代码执行完成后),会开始执行事件循环。
那么事件循环是为了解决什么呢?
为了在线程运行过程中,能接收并执行新的任务,JavaScript 采用事件循环机制, JavaScript 创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。
2、幕后线程( Background Threads )
当我们需要等待花费的时间比较多的一些异步任务,就会被交到幕后线程完成,比如 AJAX请求返回响应结果,或者 setTimeout 时间到了,等到完成之后就会返回该线程的回调函数放到任务队列。而且在幕后线程中,这里面的执行不会影响主线程的执行。解决了等待时间过长找出的阻塞问题。
异步回调 ,往往有两种方式
- 通过将异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。
- 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。
3、任务队列( Tack Queue )
任务队列也叫事件队列或消息队列,任务队列中的任务是通过事件循环系统来执行的
当我们的幕后线程完成就会回调到任务队列(异步回调)。
可以看出任务队列,如其名称,他的数据结构是一个队列,符合 “ 先进先出 ” 的特点。读取任务只能从队列头部,而添加任务只能冲队列尾部。
在这里,任务队列和幕后线程以及,主线程的执行栈,三者已经解决了基本的问题了。
4、宏任务
常见的宏任务有:setTimeout, setInterval, setImmediate, I/O, UI rendering
5、 微任务队列( Macrotask )
每个宏任务都关联了一个微任务队列,而微任务的执行通常在当前宏任务快执行完成时,也就在 JavaScript 引擎准备清空调用栈的时候,JavaScript 引擎会检查该宏任务的微任务队列,然后按照顺序执行队列中的微任务。
而这里引入微任务队列是为了解决解决优先级的问题。微任务队列里的所有微任务 是在 宏任务A 之后执行的,而只有当所有的微任务执行完闭,才会进行下一个宏任务B 的执行。如果不引入微任务,那么新创建的任务就只能在下一个宏任务B 之后执行,这中间可能你需要的状态就无法在下一个宏任务B 中得到同步。
- 浏览器标准环境中(比如说谷歌webkit内核),是一个宏任务紧接着所有微任务执行。(下图就是按照浏览器标准环境)
- 在node环境中,则又不一样了,是一个类型宏任务队列执行完,再去执行微任务。
6、微任务
常见的微任务有:process.nextTick, Promise, MutationObserver(html5新特性)
面试回答🙋♂️🙋♂️🙋♂️
- JavaScript 是以单线程和同步的方式运行的
- 所以 JavaScript引擎执行代码时,会在执行栈维护一个全局上下文,所有的代码都会进入到执行栈中执行完成,但当遇到异步任务时就会将异步任务从执行栈中弹出交给其他线程来完成
- 当异步任务完成后,异步回调,会添加到任务队列中,等待执行栈中的任务完成后,如果 JS 引擎看到有任务队列有事件,那么它会为事件创建一个新的执行上下文,并将其推送到执行栈上。
- JavaScript引擎会检查任务队列里面是否有任务,有的话会取出来放到执行栈中执行,这个过程称之为一个 Tick,完成后会在结束前检查是否有微任务,有的话执行至清空微任务队列,才会进入到下一个 Tick。
十三、知识点:宏任务和微任务 ⭐⭐⭐
上面我们了解事件循环的过程,下面我们围绕 宏任务和 微任务去了解,任务队列这部分都发生了什么。
首先简单回顾一下事件循环的过程:
- 我们的任务执行分为 同步任务 和 异步任务
- 同步任务会在执行栈执行,异步的交给其他线程处理,完成后会交到任务队列中(
异步回调 ) - 执行栈中的完成后会取出任务队列的任务循环执行
在这事件循环的第三步中的循环执行,每进行一次循环操作,我们称之为 tick ,每一次 tick 步骤如下:
- 在 Tack Queue 中选择第一个任务,进行执行
- 检查其是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue
- 如果宿主为浏览器,可能会渲染页面
- 开始下一轮的 tick
一、首先宏任务,微任务通常是什么
宏任务:每次执行栈执行的代码就是一个宏任务,并且每次从任务队列中获取放到执行栈中执行的事件回调也是。
微任务:当前宏任务后,下一个宏任务之前,在渲染之前。
二、那些是宏任务,那些是微任务,他们的执行时机,和使用场景
宏任务
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
微任务
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)
- 1
- 2
- 3
- 4
三、为什么有宏任务还要微任务
很简单,当然是宏任务满足不了现在的需求啦。满足不了现在越来越多细分的操作和交互。比如下面的代码:
<script type="text/javascript">
function timerCallback2(){
console.log(2)
}
function timerCallback(){
console.log(1)
setTimeout(timerCallback2,0)
}
setTimeout(timerCallback,0)
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我们通过 setTimeout 来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务,因为如果这两个任务的中间插入了其他的任务,就很有可能会影响到第二个定时器的执行时间了。
这是我们理想的,就是输出 1 后 紧跟着是 2 ,但是实际上我们工作中运行类似的代码可能会在其中被渲染引擎插入很多的其他的任务,这就可能影响到我们的任务执行了。
所以微任务就为此诞生了,划分更细的时间粒度,便于精细操控。
那么如何实现微任务呢?
微任务的存放:当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列,用来存放微任务。微任务的产生: 有两种方式
- 使用 MutationObserver 监控某个 DOM 节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
- 用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务
微任务的执行:当前宏任务后,下一个宏任务之前,在渲染之前
面试回答🙋♂️🙋♂️🙋♂️
宏任务 和微任务 是在事件循环中定义的,一般我们把每次在执行在中执行的代码看做一个宏任务,其中宏任务是由宿主(浏览器 / node)发起的,一般有 script(整体代码) / setTimeout / setInterval / I/O / UI交互事件 / postMessage 等。
而微任务由 JavaScript 自身发起,在当前宏任务后,下一个宏任务之前执行的,一般有 Promise.then / Object.observe / MutationObserver / process.nextTick(Node.js 环境) 等。
十四、知识点:一道宏任务和微任务的测试题 ⭐
上面我们了解在 JavaScript 中执行一段代码会发生什么,需要经历的,下面我们在看一个例子去深化了解其中:
console.log("AAAA");
setTimeout(() => console.log("BBBB"), 1000);
const start = new Date();
while (new Date() - start < 3000) {}
console.log("CCCC");
setTimeout(() => console.log("DDDD"), 0);
new Promise((resolve, reject) => {
console.log("EEEE");
foo.bar(100);
})
.then(() => console.log("FFFF"))
.then(() => console.log("GGGG"))
.catch(() => console.log("HHHH"));
console.log("IIII");
//输出结果:
AAAA
CCCC
EEEE
IIII
HHHH
BBBB
DDDD
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
分析
- 一开始代码执行,输出AAAA. 1
- 第二行代码开启一个计时器t1(一个称呼),这是一个异步任务且是宏任务,需要等到1秒后提交。
- 第四行是个while语句,需要等待3秒后才能执行下面的代码,这里有个问题,就是3秒后上一个计时器t1的提交时间已经过了,但是线程上的任务还没有执行结束,所以暂时不能打印结果,所以它排在宏任务的最前面了。
- 第五行又输出CCCC
- 第六行又开启一个计时器t2(称呼),它提交的时间是0秒(其实每个浏览器器有默认最小时间的,暂时忽略),但是之前的t1任务还没有执行,还在等待,所以t2就排在t1的后面。(t2排在t1后面的原因是while造成的)都还需要等待,因为线程上的任务还没执行完毕。
- 第七行new Promise将执行promise函数,它参数是一个回调函数,这个回调函数内的代码是同步的,它的异步核心在于resolve和reject,同时这个异步任务在任务队列中属于微任务,是优先于宏任务执行的,(不管宏任务有多急,反正我是VIP)。所以先直接打印输出同步代码EEEE。第九行中的代码是个不存在的对象,这个错误要抛给reject这个状态,也就是catch去处理,但是它是异步的且是微任务,只有等到线程上的任务执行完毕,立马执行它,不管宏任务(计时器,ajax等)等待多久了。
- 第十四行,这是线程上的最后一个任务,打印输出 IIII
- 我们先找出线程上的同步代码,将结果依次排列出来:AAAA CCCC EEEE IIII
- 然后我们再找出所有异步任务中的微任务 把结果打印出来 HHHH
- 最后我们再找出异步中的所有宏任务,这里t1排在前面t2排在后面(这个原因是while造成的),输出结果顺序是 BBBB DDDD
- 所以综上 结果是 AAAA CCCC EEEE IIII HHHH BBBB DDDD
事件循环
事件循环前的任务都是同步代码的,响应时间很慢,浪费了很多的时间,js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

宏任务和微任务的区别

十四、知识点:Promise * ⭐⭐⭐
数据结构
一、知识点:在 JavaScript 中实现一个堆 *⭐⭐⭐
class Heap {
var a[]; // 数组,从下标1开始存储数据
var n; // 堆可以存储的最大数据个数
var count; // 堆中已经存储的数据个数
Heap(capacity) {
a = new Array[capacity + 1];
n = capacity;
count = 0;
}
//插入元素
insert(data) {
if (count >= n) return; // 堆满了
++count;
a[count] = data; // 最后一位插入
const i = count;
swim(a, count,i)
}
}
//从下往上堆化
swim(a,n,i) {
while (i/2 > 0 && a[i] > a[i/2]) { // 存在父节点,且子节点大于父节点
swap(a, i, i/2); // swap():交换下标为i和i/2的两个元素
i = i/2;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
二、知识点:关于二分法的两个需要注意到的地方 ⭐⭐⭐
简单来说二分法不难,但是有两个需要注意的地方防止踩坑
一、首先是注意边界条件,也就是搜索区间
-
一种是 [ left , right ]
- 因为 left 和 right 相等是有意义的,所以 while ( left <= right )
- 另外 nums [ middle ] 大于targrt ,那么更新右下标 right 为 middle - 1 ,反之则为那么更新左下标 right 为 middle + 1
-
另一种是 [ left , right )
- 因为 left 和 right 相等是没有意义的,所以 while ( left < right )
- 另外 nums [ middle ] 大于targrt ,那么更新右下标 right 为 middle ,反之则为那么更新左下标 right 为 middle + 1
虽然看起来仅仅是右边是否闭合,但是带来的结果是不一样的,也对应了两种二分法的写法
二、另一个·就是中间值的,也就是 middle 的取值,这里的要注意是否溢出的问题
当我们第一反应取中间值就是简单粗暴的 middle = ( left+ right ) / 2 ,但是这里实际上不应该这么写,会导致溢出问题,我们应该写成 middle = left + (( left+ right ) / 2);
那么为什么这么写就能防止溢出呢?
首先我们有:left < right
所以有: right - left > 0 ; 又有 : left + (right - left) = right
所以 left + (right - left)/2 <= right
所以不会产生溢出吗,因为每一步都限制了 right 的范围
而要是 middle = ( left+ right ) / 2
就会有 left+ right >= right
就可能导致溢出的问题出现
三、知识点:各类数据结构的区别和使用场景 * ⭐⭐⭐
选择不同的数据结构往往是为了对应我们常见的 增 / 删 / 改 / 查 这四大操作,而不同的数据结构对于不同的操作效率也是不同的,为了较好的适应我们的需求,就需要了解各种数据结构的特点,来做选择。
一、 数组
1. 场景
- 数据量少,且规模已知,随机访问修改,不经常删减插入数据。
2.优缺点
- 数组的特点决定了他,不适合用于要对数据删减的,插入的。因为当我们在数组中间插入增加数据,那么这个数组后面的所有都需要变更移动。而数组的下标,也让数组在查找中又较好的性能
二、 链表
1. 场景
- 链表解决的主要就是数组的两大问题,不需要知道数据的规模,便于插入
2.优缺点
- 链表在插入添加的场景是比较高效的,因为链表只需要改变指针的指向就可以实现,适应于频繁的插入操作。同时链表是不需要知道数据规模的。但是在查找中是没有下标,相对数组比较慢。
还有很多其他的,欠着先吧~~
四、知识点:为什么分为 堆 和 栈 呢?⭐⭐⭐
一、为什么区分 堆 和 栈?
之所以区分堆 和 栈,主要是因为对内存不同的需求而决定的。内存需求我们分为 静态分配 和 动态分配
1、静态分配(栈)
静态分配往往是我们能确定我们需要的内存大小 ,在 JavaScript 中往往这一类就是 基本数据类型。而往往在 JavaScript 中基本类型是保存在栈中。因为 JS引擎 知道大小不会变,所以它们将为每个值分配固定大小的内存。
2、动态分配(堆)
动态分配往往就是我们不能确定我们需要的内存大小,比如我们定义的对象等类型,与栈不同,JS引擎不会为这些对象分配固定大小的内存空间。相反,将根据需要分配更多的内存空间。
二、堆 和 栈 的区别
1、内存大小
栈中的变量一般都是已知大小或者有范围上限的。而堆中存储的对象类型数据大小这方面,一般都是未知的。
2、读取速度
一般来说栈内存是线性的,有序存储的,容量小,但系统分配效率高,而堆内存是乱序的,容量大,在堆内存分配内存区间,而在栈内存保存指针,根据指针查找获取。
五、知识点:Set 和 Map ⭐⭐⭐
一、Set
1、简介
- Set 和 Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。
2、最大特点:
- 能保证里面的值不重复
方法
- add():添加值
- delete():返回true或false来告知是否成功删除
- has():判断是否存在
- forEach():遍历
二、Map
1、简介
- 是一种键值对的结构,具有极快的查找速度
2、方法
- set(key ,value):添加(如果添加的是已经存在的值,就会覆盖)
- get(key):获取
- size():获取长度
- has(key):判断是否存在
- delete(key):删除
- forEach/for…of:遍历
六、知识点:前序遍历 / 中序遍历 / 后序遍历 *⭐⭐⭐⭐⭐
前序遍历(根–>左–>右)
1.访问根节点
2.前序遍历左子树
3.前序遍历右子树
- 1
- 2
- 3
- 4
- 5
中序遍历(左–>根–>右)
1.中序遍历左子树
2.访问根节点
3.中序遍历右子树
- 1
- 2
- 3
- 4
- 5
后序遍历(左–>右–>根)
1.后序遍历左子树
2.后序遍历右子树
3.访问根节点
- 1
- 2
- 3
- 4
- 5
七、知识点:扁平化数组的方法 *⭐⭐⭐
手写函数
一、知识点:手写深拷贝 ⭐⭐⭐⭐⭐
一、浅拷贝的实现
- object.assigin() //该方法在只有一层时,进行的是深拷贝
- Array.prototye.concat
- Array.prototye.slice
二、深拷贝的实现
实现的思路:
- 判断是否为对象或null
- 新建变量开辟地址 ,用来保存复制过来的值
- 判断类型,来赋值是数组还是对象
- 循环递归调用,解决嵌套
// 我的深拷贝
//数据
const obj1 = {
age : 20,
name : 'aasfa',
address : {
city : 'beijing',
county : 'china'
},
arr: ['s','g' , 'f']
}
//我的深拷贝
function myDeepCope (obj = []) {
//判断是否为对象或null,
if (typeof obj !== 'object' || obj == null ) {
return obj
}
// 用来开辟新的地址,保存复制过来得到值
let result;
// 判断类型
if(obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 避免继承以外的属性,只能当前的
if ( obj.hasOwnProperty(key)) {
result[key] = myDeepCope(obj[key])
}
}
return result
}
//测试
let aa = myDeepCope(obj1);
console.log(aa.name);
aa.name = 'fasdgasdag'
console.log(aa.name);
console.log(obj1.name);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
二、知识点:节流防抖 * ⭐⭐⭐⭐⭐
其他
一、知识点:节流和防抖 ⭐⭐⭐⭐⭐
一、因为什么而诞生的:
可以说是经常会在面试中问到的一个知识点,并且在实际业务中也会频繁用到
防抖:比如我们的输入框中,表单验证,按键提交等。当我们打开淘宝的输入框搜索时,每输入完下面就会出现相关的提示,这里就用到了防抖。
具体是什么呢?防抖就是当持续触发事件时,会等到停止后一段时间才开始执行。为什么需要这样呢?比如,要是我们点击一个按键,我们是不是只需要提交一次,要是我们疯狂点击,是不是就会疯狂触发。还有输入搜索,我们是不是应该输入结束才开始调用接口搜索,而不是每输入一个字就调用一次。使用防抖函数后,就使得只会触发一次,避免了无意义的触发。
节流:和防抖相比,节流是持续触发事件,会每隔一段时间,才执行执行一次,比如在DOM元素的拖拽功能中,射击游戏,计算鼠标移动距离中。当我们计算鼠标的移动距离,要是没有使用节流函数,那么每移动1px,就会调用计算。我们可想而知,当我们轻轻滑动。就会调用触发无数次了,会给服务器带来极大的压力。但我们使用后,比如设置间隔时间,持续触发那么他就会每隔这个时间段触发一次。大大减少服务端压力。
二、如何实现,解决了什么:
具体掌握程度,不仅要了解两者区别和作用,同时还需要能手写实现,
面试回答🙋♂️🙋♂️🙋♂️
防抖和节流的最大区别在于他们的单位时间内执行的次数,防抖单位时间只会执行一次,而节流单位时间会执行指定次数。
我们往往是在输入框,提交按键中使用防抖,比如我们点击一个按键,只需要提交一次,但要是我们疯狂点击,就会疯狂触发。还有输入搜索,我们应该输入结束才开始调用接口搜索,而不是每输入一个字就调用一次。使用防抖函数后,就使得只会触发一次,避免了无意义的触发。
而我们往往在 DOM元素的拖拽功能,射击游戏,计算鼠标移动距离中使用节流。要是没有使用节流函数,那么每移动1px,就会调用计算,会给服务器带来极大的压力。使用节流后,设置间隔时间,当持续触发,他就会每隔单位时间触发一次,大大减少服务端压力。
深化文章
二、知识点:深拷贝和浅拷贝 ⭐⭐⭐⭐⭐
因为什么而诞生的:
首先我们应该知道,数据基本分为基本数据和引用数据。
引用型数据的特点在栈中保留一个指针,指向在堆中的真实位置,而真实数据是存储在堆中的。基本类型则是都存储在栈中的。
所以我们的问题来了,对于引用类型,我们拷贝往往拷贝的只是存储在栈中,指向堆中真实数据的的指针,也就是它的引用地址,并不是拷贝了真实的数据。所以当我们引用类型的真实数据发生改变,所有的值都会发生改变,因为他们都是指向同一个真实数据。对于这种情况我们称之为浅拷贝。
而对于深拷贝的话,会将其完全复制,不会指向共同的内存,修改不会互相影响。

总结深拷贝和浅拷贝的区别在于:深拷贝完全复制,不共用内存,修改不会相互影响,浅拷贝仅仅复制引用地址,修改会互相影响。所以当我们讨论深浅拷贝的时候,往往是针对引用类型来说的。
六、知识点:各类图片格式都有哪些? ⭐⭐⭐⭐⭐
我们常见的图片格式有下面几种:
格式 优点 缺点 适用场景 GIF 文件小,支持动画,透明,无兼容性问题 只支持256种颜色 logo、icon、动图 JPG 色彩丰富、文件小 有损压缩 色彩丰富的图 PNG 无损压缩、支持透明、简单图片尺寸小 不支持动画、色彩丰富的图片尺寸大 logo、icon、透明图 SVG 随意伸缩不牺牲质量、支持动画、比前三者小 复杂度高减慢渲染速度 图标
可以说上面几种以及他们的特点我们大多数人都能说的出来,但是仅仅这样是不够的,这只是说明你和大多数人一样,我们需要的是别人知道的我们也知道,别人不知道的我们也知道!下面我们看两种新的图片格式:
-
Webp:WebP最初在2010年由谷歌发布,目标是在相同JPEG的效果下减少文件的大小,减少体积可以大大加快在网络的传输所需的时间,而在质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。如今Facebook Ebay等知名网站已经开始测试并使用WebP格式。唯一的缺点是WebP格式图像的编码时间“比JPEG格式图像长8倍,目前支持的浏览器除了 Safari 外基本都支持。
-
Apng:Apng是在2004年诞生的,也就是是PNG的位图动画扩展,可以实现png格式的动态图片效果。如今在 Web 上,Firfox , Safari 和 Chrome 都是是支持 APNG 的~
三、知识点:Canvas 和 SVG 以及 WebGl的区别 *⭐⭐⭐
一、SVG:
SVG可缩放矢量图形,通过XML描述的2D图形,SVG基于XML就意味着SVG DOM中的每个元素都是可用的,可以为某个元素附加Javascript事件处理器。在SVG中,每个被绘制的图形均被视为对象。如果SVG对象的属性发生变化,那么浏览器能够自动重现图形。
其特点如下:
- 不依赖分辨率
- 支持事件处理器,使用 js 给元素添加事件
- 最适合带有大型渲染区域的应用程序(比如谷歌地图)
- 复杂度高会减慢渲染速度(任何过度使用DOM的应用都不快)
- 不适合游戏应用
二、Canvas:
Canvas是画布,通过Javascript来绘制2D图形,基于像素,通过画布和绘制的API实现,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。
其特点如下:
- 依赖分辨率
- 不支持事件处理器
- 弱的文本渲染能力
- 能够以.png或.jpg格式保存结果图像
- 最适合图像密集型的游戏,其中的许多对象会被频繁重绘
三、Canvas 和 SVG的区别
Canvans SVG 历史 较新,由Apple私有技术发展而来 历史悠久,2003年成为W3C标准 功能 简单,2D绘图API 功能丰富,各种图像,动画等 特点 基于像素,只能脚本驱动 矢量,XML,CSS,元素操作 对象 基于像素 基于图像对象 驱动 单个HTML元素 多个图形元素(Path,Line等) 性能 适合小面积,大数量 适合大面积,小数量 模糊 基于像素,也就是分辨率,放大会模糊失真 矢量,改变大小不会失真
四、使用场景选择

四、知识点:iframe有那些缺点?*⭐
- iframe会阻塞主页面的Onload事件
- 搜索引擎的检索程序无法解读这种页面,不利于SEO
- iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
- 使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript动态给iframe添加src属性值,这样可以绕开以上两个问题
五、知识点:文档声明(Doctype)和(!Doctype html)有何作用?*⭐
一、文档声明(Doctype)和(!Doctype html)有何作用
- 文档声明的作用:文档声明是为了告诉浏览器,当前HTML文档使用什么版本的HTML来写的,这样浏览器才能按照声明的版本来正确的解析。
- < !Doctype html > 的作用就是让浏览器进入标准模式,使用最新的HTML5标准来解析渲染页面;如果不写,浏览器就会进入混杂模式,我们需要避免此类情况发生。
六、知识点:严格模式与混杂模式如何区分?*⭐
二、 严格模式与混杂模式的区分:
- 严格模式:又称为标准模式,指浏览器按照W3C标准解析代码;
- 混杂模式:又称怪异模式、兼容模式,是指浏览器用自己的方式解析代码。混杂模式通常模拟老式浏览器的行为,以防止老站点无法工作;
- 区分:网页中的DTD,直接影响到使用的是严格模式还是浏览模式,可以说DTD的使用与这两种方式的区别息息相关。
- 如果文档包含严格的DOCTYPE,那么它一般以严格模式呈现(严格DTD——严格模式);
- 包含过渡DTD和URI的DOCTYPE,也以严格模式呈现,但有过渡DTD而没有URI(统一资源标识符,就是声明最后的地址)会导致页面以混杂模式呈现(有URI的过渡DTD——严格模式;没有URI的过渡DTD——混杂模式);
- DOCTYPE不存在或形式不正确会导致文档以混杂模式呈现(DTD不存在或者格式不正确——混杂模式);
- HTML5没有DTD,因此也就没有严格模式与混杂模式的区别,HTML5有相对宽松的法,实现时,已经尽可能大的实现了向后兼容(HTML5没有严格和混杂之分)。
总之,严格模式让各个浏览器统一执行一套规范兼容模式保证了旧网站的正常运行。
七、知识点:前端命名方式特点建议 *⭐
前两天做个项目的时候,发现一急起来,命名也乱七八糟了,这里整理一下
首先几点:
- 考虑为元素命名其本身的作用或”用意”,达到语义化。不要使用表面形式的命名,命名的方式能描述出正在做的事情
如:red/left/big等,而是比如一个搜索的按键:searchButton
- 组合命名规则,[元素类型]-[元素作用/内容]
如:搜索按钮: btn-search
登录表单:form-login
新闻列表:list-news
- 涉及到交互行为的元素命名:,凡涉及交互行为的元素通常会有正常、悬停、点击和已浏览等不同样式,命名可参考以下规则:
鼠标悬停::hover 点击:click 已浏览:visited
如:搜索按钮: btn-search、btn-search-hover、btn-search-visited
- 组件的命名要大写
八、知识点:git 以及 git pull 和 git fetch 有什么区别?⭐⭐⭐⭐⭐
一、关于 git 常用指令
git clone:当我们刚入职的时候第一件事就是把公司的代码克隆下来,该命令就是git init:当我们一个项目没有配置 git的时候,就可以使用该命令来初始化,会生成一个隐藏文件git add:当我们改动了代码,提交的时候这一步就会添加到暂存区git commit:当我们推送代码就需要使用该命令能告诉别人这个推送是做了什么操作git pull:该命令是用来更新我们本地代码的,我们在push前一定要先pull避免发生冲突git push:当我们pull了之后就是通过该命令来推送代码到远端的仓库git fetch :该命令是用来更新本地的代码,但是不同于pull不会进行合并git checkout:该命令是用来切换分支的,当我们获取到自己的需求任务的时候,我们可以创建自己的分支来开发,这样不会导致最后因为你的开发没有完成影响别人的发布git branch :显示本地的所有分支git status:查看状态
二、git pull 和 git fetch 有什么区别?
1、git pull 和 git fetch 的作用
先回顾两个命令的定义
- git fetch 命令用于从另一个存储库下载对象和引用
- git pull 命令用于从另一个存储库或本地分支获取并集成(整合)
git的工作流程

可以看出
git fetch 是将远程主机的最新内容拉到本地,用户在检查了以后决定是否合并到工作本机分支中
但是
git pull 则是将远程主机的最新内容拉下来后直接合并,即:git pull = git fetch + git merge,这样可能会产生冲突,需要手动解决
2、git pull 和 git fetch 的区别
相同点:
- 在作用上他们的功能是大致相同的,都是起到了更新代码的作用
不同点:
- git pull是相当于从远程仓库获取最新版本,然后再与本地分支merge,即git pull = git fetch + git merge
- 相比起来,git fetch 更安全也更符合实际要求,在 merge 前,我们可以查看更新情况,根据实际情况再决定是否合并
八、知识点:网站优化的方式*⭐⭐⭐⭐⭐
这是一个很常见的问题了,那么如果需要优化我们一般有什么的方式呢?
-
使用服务端渲染
往往我们是在一些网站的官网就会使用到服务网段渲染,对于一些交互多的是使用客户端渲染的,使用服务端渲染有什么好处呢?首先一点是便于 SEO,服务端渲染是服务器一次性返回页面完成HTML拼接的,所以这会便于浏览器搜索引擎爬取信息,便于网站排名。
-
使用缓存
使用缓存也是能极大提高用户体验的方式
-
减少对 DOM 的操作
-
使用CDN 内容分发
-
减少发起 HTTP请求
-
IP缓存,减少DNS查找次数
九、知识点:网页SEO 的应用 ⭐⭐⭐⭐⭐
单论 SEO 我们可能不明白是什么,但是实际上 SEO 存在于各个地方,我们很多的操作都是需要考虑到 SEO ,比如在服务端渲染和客户端渲染中,比如搜索引擎爬取中。下面我们分几个问题来了解:
一、什么是 SEO
SEO 也就是 Search engine optimization,中文是 搜索引擎优化,是通过了解搜索引擎的运作规则来调整网站,以及提高网站在有关搜索引擎中排名的方式。因为用户往往是只留意搜索结果最前面的几条结果,所以很多网站都希望通过各种方式来影响搜索引擎的排序,使得设计的网站有更好的排名。
二、实现优化 SEO的方式
首先我们想要好的排名最主要的还是我们网站的质量够高,能吸引用户,这才是长久的方案。但是除此之外,我们也要从网站的架构上动手,使得便于引擎机器人爬取。
首先是关键词:这里面关键就是 mate 标签的 TDK,分别是指 T(titel:标题) , D(descire:描述),K(keyword:关键词),有两个原则:
- 1、TDK需要能够爬取到,不能代码封装起来。
- 2、代码里的TDK需要和显示在浏览器TDK一致。
其次是标签:避免资源浪费,集中权重,从而达到促进排名的作用。比如 H1标签在一个页面中只有一个。使用 nofollow 标签,避免给不重要的页面或者站外链接传递权值,
还有很多的方式,比如面包屑,对于单页面网站的优化,404 页面、sitemap 文件、自动主动推送等等。
面试回答🙋♂️🙋♂️🙋♂️
关于 SEO 是一种搜索引擎优化,通过了解搜索引擎的运作方式来调整网站,已获得更好的搜索排名。
想实现好的搜索排名首先最主要的是我们网站的质量,能不断产出好的内容,以及吸引用户的设计等。除此之外,在我们前端开发中可以从一些网站的够着中优化。比如关键词,在我们的mate标签中 优化 TDK,其次在一些标签中,避免资源浪费,集中权重,比如一个网页只使用一个 H1 标签,对于不重要的或者外部的链接 使用 nofollow 标签等等。
、 知识点:说一说设计模式
、知识点:如何测试软件质量*
、知识点:我的实习经历*
、知识点:实现懒加载*
、 知识点:关于面试中的行测题
相信大家在面试中都会遇到的,那就是行测题,是不是初次的时候,一脸懵逼,我来面得是技术,为啥还有这玩意~没办法太卷了,下面放点我在准备行测题找的一些资源吧
、知识点:懒加载 和 预加载的区别
、知识点:实现虚拟列表
、知识点:JSON 和 XML 的区别 *
感觉在面试中问到这个的不多,但是在笔试中哈市会比较常遇到的,下面来了解一下这两者的特点和使用场景
一、 JSON 和 XML
二、 JSON 和 XML 的区别
、知识点:进制转换 *
、知识点:了解的前端的基本知识简介 *
一、VUE
二、Webpack
三、Babel
四、TypeScript
五、Nodejs
六、SCSS
七、EsLint
八、Axios
、知识点:AJAX 和 Axios 的简单学习*
ajax 和 fetch 的区别
、知识点:webpack的简单学习*
其实这点和上面的同样现阶段了解的比较少,毕竟比如 webpack, 我们用脚手架搭建好一个项目,就配置好了,直接运行就能打包了,这是一点。同时我个人觉得现阶段学习到还不能很常用到,所以也没很好的掌握。但是这一部门技术还是需要好好学的,后面自己也会加以练习学习。现阶段就先简单了解,不然要是面试问到,啥都说不出来就尴尬了,其实这属于前端工程化的部分,感觉在有一定能力后再去学习会更好理解,类似的还有 babel 等,下面进入正题吧~
、知识点:如何部署前端项目 *
、知识点:自定义指令 *
、知识点:自执行函数*
总结
提示:这里对文章进行总结:
-
相关阅读:
ElasticSearch7.3学习(十七)----搜索结果字段解析及time_out字段解析
Apache Jmeter压力测试与性能监控,监测cpu、内存、磁盘、网络
Oracle实现主键字段自增
RNN/LSTM (一) 实践案例
【MySQL入门指北】主从复制及读写分离
【C++初阶(二)】缺省参数&函数重载
121. 买卖股票的最佳时机
java毕业设计成品基于SpringBoot美容院预约管理系统
OpenAI在都柏林开设办事处,以扩大欧洲业务
【AI视野·今日CV 计算机视觉论文速览 第268期】Mon, 16 Oct 2023
-
原文地址:https://blog.csdn.net/weixin_44181180/article/details/123588339