🎉前端每日一题~🎉
# 🎉前端每日一题~🎉
22/1-22/3 22年第一季度的每日一题~
在下,春招冲刺人!
# 更新-不想再机械地去刷这种“每日一题”了 而是每天下班结束后总结、学习一些计算机基础、刷一些算法、前端er需掌握的知识点🙂八股文怪没营养的~
# 1/12 每日一题 (opens new window)
更多内容戳标题 (opens new window)见讨论区嗷!
1、CSS篇:定位中,absolute 与 fixed 共同点与不同点 2、JavaScript篇:闭包的概念及特点 3、算法:说一下递归和迭代的区别是什么,各有什么优缺点? 4、力扣101.对称二叉树:给你一个二叉树的根节点root,检查它是否轴对称。
# 1、CSS篇:定位中,absolute 与 fixed 共同点与不同点
共同点:
改变行内元素的呈现方式,将 display 置为
inline-block
使元素脱离普通文档流,不再占据文档物理空间,触发BFC
覆盖非定位文档元素
不同点:
absolute 与 fixed 的根元素不同,absolute 的根元素可以设置,fixed 根元素是浏览器。
在有滚动条的页面中,absolute 会跟着父元素进行移动,fixed 固定在页面的具体位置。
absolute是相对于离它最近的有相对定位的父元素进行定位(一般使用子绝父相,如果没有定位的父元素则相对于浏览器窗口);fixed是相对于浏览器窗口定位。
# 2、JavaScript篇:闭包的概念及特点
闭包的知识点有很多有趣的点!这里建议多看一些**例子 (opens new window)**学习一下,但也别太纠结!
- 1、概念
【1】闭包函数:声明在一个函数中的函数,叫做闭包函数,闭包函数可以访问其他函数内部变量。
【2】内部函数引用外部函数的数据,并且外部函数执行就会产生闭包。
【3】闭包:内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。利用闭包可以突破作用域链。
【4】使用闭包一般是为了设计私有的方法和属性。
2、特点(优点)
- 让外部访问函数内部变量成为可能;
- 可以避免使用全局变量,防止全局变量污染;
3、缺点
- 【1】可能导致内存溢出(面试高频)
var obj = {}; for(var i = 0; i < 10000; i++){ obj[i] = new Array(1000000);// new Array(1000000)定义一个长度为1000000的数组 console.log('------') }// 这个代码会导致内存溢出 浏览器会做一个崩溃的设置 终止程序
内存溢出是指存储的数据超出了指定空间的大小,这时数据就会越界
- 【2】可能会导致内存泄露
function fn1(){ var arr = new Array[10000000] function fn2(){ console.log(arr.length); } return fn2; } var f = fn1(); f();// fn1形成闭包 本该释放的arr变量现在不会被释放!
内存泄漏的意思:变量占用内存的事件可能会过长(毕竟延长了局部变量的生命周期嘛~ 申请的内存空间没有被正确释放,导致后续程序里这块内存被永远占用(不可达))——
简单来说就是:闭包会常驻内存,增加内存使用量。
所以需要及时释放——
【2.1】解决内存泄漏的方法让内部函数对象f成为垃圾对象!!
f = null;// 解放空间!
【2.2】我们可以利用垃圾回收机制销毁内存中的闭包或者手动销毁,上面提到了手动销毁,而垃圾回收机制有如下方法——
- 标记清除法
- 循环计数法
3、算法:说一下递归和迭代的区别是什么,各有什么优缺点?
这里感觉非常新奇呐!之前没有研究过!
(一)定义:
递归: 递归常被用来描述以自相似方法重复事物的过程,在数学和开发中,指的是在函数定义中使用函数自身的方法;递归实际上不断地深层调用函数,直到函数有返回才会逐层的返回,递归是用栈机制实现的,每深入一层,都要占去一块栈数据区域,因此,递归涉及到运行时的堆栈开销(参数必须压入堆栈保存,直到该层函数调用返回为止),所以有可能导致堆栈溢出的错误;但是递归编程所体现的思想正是人们追求简洁、将问题交给计算机,以及将大问题分解为相同小问题从而解决大问题的动机。递归,还有个尾调用优化,尾调用优化就是如果本次调用的返回值,是子调用的返回值的话,本次调用就可以直接出栈了,不需要进行嵌套。就可以实现栈深为1的递归调用。递归从字面可以其理解为重复“递推”和“回归”的过程(递推:层层推进,分解问题;回归:层层回归,返回较大问题的解)
简单来说:程序调用自身称为递归
- 这里可以看我之前写的一篇剖析递归过程的文章掌握递归调用栈思想 由浅入深研究递归🎉 - 掘金 (juejin.cn) (opens new window),带上图片和例子一起理解
迭代: 是重复反馈过程的活动,其目的通常是为了接近并到达所需的目标或结果。每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。迭代是顺序的,不涉及调用栈操作,前面的代码不会被后面代码影响,递归是涉及到调用栈的,遵循先入栈后结束后入栈先结束原则,前面的函数调用会阻塞,要在后面的调用返回值后才能继续执行,所以迭代的好处就是栈深小,但是代码逻辑不够清晰,递归则是嵌套调用多,栈深比较大,容易爆栈,但代码结构会比较简洁,速度的话还是迭代快。迭代大部分时候需要人为的对问题进行剖析,分析问题的规律所在,将问题转变为一次次的迭代来逼近答案。迭代不像递归那样对堆栈有一定的要求,另外一旦问题剖析完毕,就可以很容易的通过循环加以实现。迭代的效率高,但却不太容易理解,当遇到数据结构的设计时,比如图表、二叉树、网格等问题时,使用就比较困难,而是用递归就能省掉人工思考解法的过程,只需要不断的将问题分解直到返回就可以了。
简单来说:利用变量的原值推出新值称为迭代。
(二)异同点:
了解一下递归和迭代的时间复杂度!感觉蛮加分的~
相同点:递归和迭代都是循环的一种。
不同点:
- (1)程序结构不同:递归是重复调用函数自身实现循环。迭代是函数内某段代码实现循环。 其中,迭代与普通循环的区别是:迭代时,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。
- (2)算法结束方式不同(递归的一大要素——结束条件):递归循环中,遇到满足终止条件的情况时逐层返回来结束。迭代则使用计数器结束循环。 当然很多情况都是多种循环混合采用,这要根据具体需求。
- (3)效率不同:在循环的次数较大的时候,迭代的效率明显高于递归
- (4)运行过程不同,如果是循环迭代的话,这个整个就在主函数的或者在调用函数的栈空间里面,如果是递归的话,它会不断的申请函数调用的栈空间,在计算的过程中,计算一个结果,退一层栈,递归过程,在调用的时候有可能会出现栈的溢出。
- (5)理论上递归和迭代时间复杂度方面是一样的,但实际应用中(函数调用和函数调用堆栈的开销)递归比迭代效率要低。
(三)优缺点
递归的
- 优点: 大问题转化为小问题,可以减少代码量,同时代码精简,可读性好。
- 缺点: 递归调用浪费了空间(递归调用栈),而且递归太深容易造成堆栈的溢出。
迭代的
- 优点: 就是代码运行效率好,因为时间只因循环次数增加而增加,而且没有额外的空间开销。
- 缺点: 就是代码不如递归简洁
# 4、力扣101.对称二叉树 (opens new window)
DFS深搜递归法
var isSymmetric = function(root) {
// 先dfs深搜到最下面,验证局部对称性
function dfs(left, right) {
// 遍历到叶节点,返回true
if(left === null && right === null) {
return true
}
// 遍历到只有一个子节点的节点,返回false
if(left === null || right === null) {
return false
}
// 遍历到两个不相等的节点,返回false
if(left.val !== right.val) {
return false
}
// 递归比较左节点的左孩子和右节点的右孩子&&左节点的右孩子和右节点的左孩子
// 这一步 将递归函数dfs不断加入递归调用栈,进行局部对称性的验证
return dfs(left.left, right.right) && dfs(left.right, right.left)
}
return dfs(root.left, root.right)
};
队列+迭代法
var isSymmetric = function(root) {
//迭代方法判断是否是对称二叉树
//首先判断root是否为空
if(root===null){
return true;
}
let queue=[];
queue.push(root.left);
queue.push(root.right);
while(queue.length){
let leftNode=queue.shift();//左节点
let rightNode=queue.shift();//右节点
if(leftNode===null&&rightNode===null){
continue;
}
if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
return false;
}
queue.push(leftNode.left);//左节点左孩子入队
queue.push(rightNode.right);//右节点右孩子入队
queue.push(leftNode.right);//左节点右孩子入队
queue.push(rightNode.left);//右节点左孩子入队
}
return true;
};
栈+迭代
var isSymmetric = function(root) {
//迭代方法判断是否是对称二叉树
//首先判断root是否为空
if(root===null){
return true;
}
let stack=[];
stack.push(root.left);
stack.push(root.right);
while(stack.length){
let rightNode=stack.pop();//左节点
let leftNode=stack.pop();//右节点
if(leftNode===null&&rightNode===null){
continue;
}
if(leftNode===null||rightNode===null||leftNode.val!==rightNode.val){
return false;
}
stack.push(leftNode.left);//左节点左孩子入队
stack.push(rightNode.right);//右节点右孩子入队
stack.push(leftNode.right);//左节点右孩子入队
stack.push(rightNode.left);//右节点左孩子入队
}
return true;
};
# 1/13 每日一题
1.箭头函数与普通函数的区别?
2.this指向哪⾥?
3.扩展运算符的作用及使用场景?
仓管来加道经典题 大家来瞅一眼呗hh 昨天做了树 今天做个超级经典数组题吧!
# 1.箭头函数与普通函数的区别?
1.箭头函数的参数只有一个时,可以省略小括号(但是!这里最好是带上括号 (opens new window)哦!),函数里面的执行语句只有一条时,可以省略花括号(但是!如果回调函数没有return 则最好加上大括号 (opens new window) ——减少副作用) 2.箭头函数本身没有this,它会继承作用域链上一层的this 3.箭头函数不能使用call, bind, apply来改变this指向
# 1:写法不一样
function foo() {
console.log('1')
}
let foo = ()=> {
console.log('1')
}
# 2:普通函数存在变量提升的现象 箭头函数只会被提升变量
//普通函数
foo()
function foo() {
console.log('1')
}
//箭头函数
foo() //报错 foo is not a function
let foo = ()=> {
console.log('1')
}
# 3:箭头函数不能作为构造函数使用
let Person = (name) => {
this.name = name
}
let xiao_ming = new Person('小明')
console.log(xiao_ming.name) //undefined
# 4:两者this的指向不同
普通函数的this指向的是谁调用该函数就指向谁
箭头函数的this指向的是在你书写代码时候的上下文环境对象的this,如果没有上下文环境对象,那么就指向最外层对象window。
# 5:箭头函数的arguments指向它的父级函数所在作用域的arguments
function foo() {
console.log(arguments)
let foo1 = () => {
console.log(arguments)
}
foo1()
}
foo('test')
//[Arguments] { '0': 'test' }
//[Arguments] { '0': 'test' }
# 6:箭头函数没有new.target
先说明下new.target是干嘛的,这家伙是用来检测函数是否被当做构造函数使用,他会返回一个指向构造函数的引用。
因为箭头函数不能当做构造函数使用,自然是没有new.target的。
# 2.this指向哪里?
其实this指向的问题情况并不多,但是它在不同的执行条件下可能会绑定(指向)不同的对象,如果想要再深入了解,可以看一下coderwhy的这篇文章 (opens new window)
- 普通函数中:this->window
- 定时器中:this->window
- 构造函数中:this->当前实例化的对象
- 事件处理函数中:this->事件触发对象
- 在 js 中一般理解就是谁调用这个 this 就指向谁
# 3.扩展运算符的作用及使用场景
# 0.将一个数组转为用逗号分隔的参数序列。
这里其实是以下几种方法的本质啦~
console.log(...[1, 2, 3]) // 1 2 3
console.log(1, ...[2, 3, 4], 5) //1 2 3 4 5
# 1.普通函数中使用,用作参数
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [1, 2];
add(...numbers) // 3
# 2.替代 apply 方法 方便地将数组转变为可以传入函数的参数
本质也跟0是一样的~
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
# 3.合并数组
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3) // [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
# 4.与解构赋值结合
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
这个点之前还真没有注意~
const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错
# 5.将字符串转为数组
var str = 'hello';
// ES5
var arr1 = str.split(''); // [ "h", "e", "l", "l", "o" ]
// ES6
var arr2 = [...str]; // [ "h", "e", "l", "l", "o" ]
# 6.实现了 Iterator 接口的对象
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
var domList = document.querySelectorAll('span');
var array = [...domList];
# 7.数组/对象的浅拷贝
const arr = [1, 2, 3];
const copy = [...arr];
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 };
# 4、217. 存在重复元素 (opens new window)
经典的数组查重问题!
- 【1】利用判重API
indexOf
构建去重辅助数组
var containsDuplicate = function(nums) {
let newArr = []
for(let i = 0; i < nums.length; i++) {
if(newArr.indexOf(nums[i]) === -1) {
newArr.push(nums[i])
}
}
return newArr.length !== nums.length
};
执行用时较高!想必是indexOf
方法的锅咯~
- 【2】利用数据结构Set 构造只有单一元素的Set对象,判断Set对象与原数组长度是否相同
var containsDuplicate = function(nums) {
let newArr = new Set(nums)
return newArr.size !== nums.length
};
- 同理 使用哈希表记录出现的次数也可以~
这里用Set其实更好哈~主要上面刚用过 换个口儿😄
var containsDuplicate = function(nums) {
let map = new Map()
for(let i = 0; i < nums.length; i++) {
if(map.has(nums[i])) {
return true
}
else{
map.set(nums[i], 1)
}
}
return false
};
时间复杂度O(N)
空间复杂度O(N)
- 【3】排序后冒泡比较
这里可以拓展炫技手写个排序出来?😏
var containsDuplicate = function(nums) {
nums.sort((a, b) => a - b)
for(let i = 0; i < nums.length - 1; i++) {
if(nums[i] === nums[i + 1]) {
return true
}
}
return false
};
时间复杂度O(N*log~N~)
空间复杂度O(log~N~) 在这里应当考虑排序时递归调用栈的深度。
V8 引擎 sort 函数只给出了两种排序 InsertionSort 和 QuickSort,
- 数量小于10的数组使用
InsertionSort
-插入排序- 比10大的数组则使用
QuickSort
-快速排序详情见V8 引擎array源码 (opens new window) 710行开始 快排在760行处
- 【4】说干就干!来手写个快排!参考之前写过的一篇Java题解 (opens new window)(写得贼详细 每一步都拆分开来说了!),这篇文章是参考的leetcode主站的一篇优秀图解 by 袁厨 (opens new window)~(好家伙连环参考)
快排分以下几步
1.选出基准值
2.使用填坑法 (opens new window),写一个partition函数将数组分为小于基准值和大于基准值两部分
3.递归完成快排!
下面代码里这个注释很清楚了吧!
另外还写了个题解 (opens new window) 搭配图看着更舒服哈~
var sortArray = function(nums) {
quicksort(nums, 0, nums.length - 1)// 调用快排方法
return nums
};
var quicksort = function(nums, low, high) {
if(low < high) {
let index = partition(nums, low, high)// 得到用来将数组分成两部分(左面全小于index 右面全大于index)的索引
quicksort(nums, low, index - 1)// 以第一轮得出的index为基准划分出左半区和右半区 对数组的左半区进行递归 将其全部变为有序
quicksort(nums, index + 1, high)// 同理左半区
}
}
var partition = function(arr, low, high) {
let pivot = arr[low]// 选定第一个元素为基准值 把它拿出来 即为“挖坑”
while(low < high) {
// 【1】 挖了坑就需要填坑~从high指针开始向左找
while(low < high && arr[high] >= pivot) {
high--
}
arr[low] = arr[high]// 一旦找到比坑对应值pivot小的 就扔到low那侧的坑里
// 【2】 同【1】从low指针开始向右找填坑值
while(low < high && arr[low] < pivot) {
low++
}
arr[high] = arr[low]// 一旦找到比坑对应值pivot大的 就扔到high那侧的坑里
//(刚刚这侧有一个值去填low那侧的坑了 所以出现了一个坑位~)
}
// 经过上面【1】【2】的不断迭代 low===high 此时这个位置即为基准位置
arr[low] = pivot
return low// 分区成功!返回定海神针~(此时low=high哦~)
}
再拓展补充下~
不稳定的四种排序方法 快选希堆
最快的排序方法是归并排序 还有一个不常见的堆排序! 时间复杂度为 n×log~n~
- 快速排序的平均时间复杂度是n×log~n~
归并排序和快速排序的区别:
- 归并是先拆开 从底下往上面排
- 快排是每次都打乱一下 从上往下排
# 1/14 每日一题
1.讲一下强缓存和协商缓存的区别 2.如何解决跨越问题 3.对事件委托的理解以及其使用场景 4 算法,最大数
# 1.讲一下强缓存和协商缓存的区别
强缓存策略和协商缓存策略
字节二面原题orz 当时听都没听说过
”啊?强缓存 对应的是弱缓存么?😫“ ”对应的是协商缓存😑😑“
在缓存命中时都会直接使用本地的缓存副本;
它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。
- 浏览器首先会根据请求的信息判断强缓存是否命中,【1】如果命中则直接使用本地资源的副本。如果不命中则【2】根据头信息向服务器发起请求,使用协商缓存——
- 【3】如果协商缓存命中的话(资源没有过期,服务器响应报文状态码为304 临时重定向),则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则服务器返回最新的资源给浏览器。
# 缓存技术实现
对于一些具有重复性的HTTP请求 比如每次请求得到的数据都是一样的 我们就可以把这对请求-响应的数据都缓存在本地 那么下次就直接读取本地的数据 不必再通过网络获取服务器的响应了~
这样的话HTTP/1.1的性能肯定可以获得肉眼可见的提升!
总结一下上面所说的 避免发送HTTP请求的方法 就是通过==缓存技术==来实现 HTTP设计者早在之前就考虑到了这点 因此HTTP协议的头部有不少是针对缓存的字段
# 缓存技术实现细节
再来刨根问底一下 缓存是如何做到的呢?
【1】客户端会把第一次请求以及相应的数据保存在本地磁盘上
其中将请求的URL作为key 而响应作为value 两者形成映射关系 URL->响应
这样 当后续发起相同的请求时 就可以先在本地磁盘上通过key查到对应的value 也就是响应 (前提:资源没有过期)
# 更新缓存的资源
看到这里 新的问题又会出现了——
万一缓存的响应不是最新的,而客户端并不知情 那么该怎么办呢?
这个问题HTTP的设计者也早已考虑到了~
服务器在发送HTTP响应时 会估算一个过期的时间 并把这个信息放到响应头部中——
这样客户端在查看响应头部的信息时,【2】一旦发现缓存的响应是过期的,则就会重新发送网络请求。(强缓存命中则直接使用本地资源的副本。如果不命中则根据头信息向服务器发起请求,使用协商缓存,也就是接下来的【3】)
HTTP关于缓存说明的头部字段很多~这部分内容之后再仔细研究下 暂时不再拓展了
# 更新缓存资源细节
最后再来思考一个问题——
如果客户端从第⼀次请求得到的响应头部中发现该响应过期了,客户端重新发送请求,假设服务器上的资源并没有 变更,还是⽼样⼦,那么你觉得还要在服务器的响应带上这个资源吗?
很显然不带的话,可以提⾼ HTTP 协议的性能,那具体如何做到呢?
是啊 如果在重新发送请求的时候发现资源并没有变更 那么服务器在响应的时候应该返回什么资源呢?
【3】这个就需要我们在客户端重新发送请求时 在请求的
Etag
头部带上第一次请求的响应头部中的摘要,这个摘要是唯一标识响应的资源,当服务器收到请求后 会将本地资源的摘要(也就是最新的摘要) 与 请求中的摘要(缓存中的摘要)做个比较——
- 如果不同 说明客户端的缓存(URL->响应)已经没有价值 服务器将在响应中带上最新的资源。
- 如果相同 说明客户端的缓存还是可以继续使用的 那么服务器仅返回不含有包体的
304 Not Modified
响应 来告诉客户端“缓存的资源仍然有效哦!” 这样可以减少响应资源在网络中传输的延时!
- ==协商缓存==
通过本个问题 - “如何避免发送HTTP请求” 对4个小点的研究
我们发现每一个点都包含了“缓存”
可以看出来 缓存真的是性能优化的一把万能钥匙!
小到 CPU Cache、Page Cache、Redis Cache
大到HTTP协议的缓存~
# 2.如何解决跨域问题
1.JSONP 使用script标签向后台请求数据,只适用于get请求
2.CORS是让服务器端设置Access-Control-Allow-Origin(头部字段),这样浏览器就不会报跨域错误
3.反向代理,搭建一个自己的服务器,让自己的服务器请求数据,拿到数据之后再返回给我自己
# 3.对事件委托的理解以及其使用场景
事件委托的本质是利用了浏览器事件冒泡的机制.
因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托
事件委托的使用场景 :
1.当存在多个元素可以共用同一个监听器
2.用事件委托实现动态监控
# 4 算法,最大数
巧妙排序
var largestNumber = function(nums) {
let sorted = nums.sort((a, b) => {
if (`${a}${b}` > `${b}${a}`) {
return -1
}
else {
return 1
}
})
// 存在[0,0] [0,0,0]这种特殊情况 会出现00这种奇怪的答案,所以要删去多余的0
for (let i = 0; i < sorted.length - 1; i++) {
if (sorted[i] === 0) {
sorted.splice(i, 1)
i--// 防止删除数组中某个元素之后造成的数组塌陷
}
else {
break
}
}
return sorted.join("")
};
知识点:V8中sort函数的实现机制
sort()
方法用原地算法 (opens new window)对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的— MDN
关于
Array.prototype.sort()
,ES 规范并没有指定具体的算法,在 V8 引擎中, 7.0 版本之前 ,数组长度小于10时,Array.prototype.sort()
使用的是插入排序,否则用快速排序。在 V8 引擎 7.0 版本之后 就舍弃了快速排序,因为它不是稳定的排序算法,在最坏情况下,时间复杂度会降级到 O(n2)。
于是采用了一种混合排序的算法:TimSort 。
这种功能算法最初用于Python语言中,严格地说它不属于以上10种排序算法中的任何一种,属于一种混合排序算法:
在数据量小的子数组中使用插入排序,然后再使用归并排序将有序的子数组进行合并排序,时间复杂度为
O(nlogn)
。作者:an_371e 链接:https://www.jianshu.com/p/a557e9006186 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
# 1/18 每日一题
- cookie sessionstorage localstorage 的区别
- 浏览器的渲染过程
- 渐进增强和优雅降级
- 手写斐波那契数列
# 1.cookie & sessionstorage & localstorage 的区别
- 共同点:都是保存在浏览器端,并且是同源的,都是字符串类型的键值对
- cookie数据始终在同源的http请求中携带,每次http请求都会携带cookie,cookie的数据大小不能超过4KB
- sessionstorage和localstorage可以达到5MB
- sessionstorage:仅在浏览器窗口关闭前有效,不能长久保存
- localstorage:数据始终有效,窗口或浏览器关闭也一直存在
# 2.浏览器的渲染过程
【1】解析 HTML 构建DOM树
【2】解析CSS 构建CSSOM树
【3】利用上面两个树构建渲染树(渲染树的节点即为“渲染对象”)
【4】渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以被称作“回流”)这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
【5】上述几步过后,布局结束;最后进行绘制,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
记住这张图:
# 3.渐进增强和优雅降级
- 渐进增强:是针对低版本浏览器构建页面,保证最基本的功能,然后针对高版本浏览器进行效果,交互等改进并追加功能,达到更好的用户体验
- 优雅降级:一开始就构建完整功能,然后对低版本浏览器进行兼容适配
# 4.手写斐波那契数列
递归,记忆化搜索与动态规划_Keep Learning-CSDN博客 (opens new window)——感谢这篇很简单易懂的文章,帮我理解了
记忆化搜索+递归=(约等于)动态规划
- 记忆化搜索和递归大致思路一样,是一种自顶向下的思路
- 动态规划则是一种自底向上的思路
超简单的递归
let fb = function(n) {
if (n === 0) {
return 0
}
if (n === 1) {
return 1
}
return fb(n - 1) + fb(n - 2)
};
递归树如下,可以看到存在大量重复计算
记忆化搜索
提升效率
const memo = []
let fb = function(n) {
if (n === 0) {
return 0
}
if (n === 1) {
return 1
}
// 这一步就是记忆化搜素新添的内容,使用一个数组来保存子问题的答案——这也正是动态规划的思想
if (memo[n]) {
return memo[n]
} else {
memo[n] = fb(n - 1) + fb(n - 2)
}
return memo[n]
};
简单的动归解法
- 将原问题拆解成若干个子问题,同时保存子问题的答案——使得每个子问题只求解一次,最终获得原问题的答案~
let fib = function(n) {
let dp = [0, 1]
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
console.log(dp)
return dp[n]
};
# 1/19 每日一题
1.字面量new出来的对象和Object.create(null)创建出来的对象有什么区别?
- 数据类型检测的方式都有哪些?
- 判断数组检测的方式都有哪些?
- new的具体操作过程
# 1.字面量new出来的对象和Object.create(null)创建出来的对象有什么区别?
- new创建出来的对象会继承Object的方法和属性,创建出来的对象的隐式原型(proto)会指向Object的显示原型;
- 而Object.create(null)创建出来的对象原型为null,作为原型链的顶端,就没有继承Object的方法和属性。
# 2.数据类型检测的方式都有哪些?
(1)typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中数组、对象、null都会被判断为object,其他判断都正确。
(2)instanceof
instanceof
可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof
只能正确判断引用数据类型,而不能判断基本数据类型。instanceof
运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。
(3) constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constructor
对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor
就不能用来判断数据类型了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
# 3 判断数组检测的方式都有哪些?
- 通过
Object.prototype.toString.call()
做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- 通过原型链做判断
obj.__proto__ === Array.prototype;
- 通过ES6的Array.isArray()做判断
Array.isArray(obj);
- 通过instanceof做判断
obj instanceof Array
- 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
# 4. new的具体操作过程
红宝书权威解释
更细致的内容之前我有总结过一篇,相当于是手写new Object
吧~JS小知识 new关键字都做了什么? - 掘金 (juejin.cn) (opens new window)
# 1/20 每日一题
1、实现一个三角形 2、JavaScript为什么要进行变量提升,它导致了什么问题? 3、实现节流函数和防抖函数
# 1/21 每日一题
1.HTTP: 1.0 1.1 2.0 3.0对应的改进点,2.0实现多路复用的底层原理?
2.Js: call、apply、bind作用和区别
3.数据结构: Map 和 Set 的区别
# 1.HTTP: 1.0 1.1 2.0 3.0对应的改进点,2.0实现多路复用的底层原理?
# HTTP 1.0和 HTTP 1.1 有以下区别:
- 连接方面,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。
- 资源请求方面,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- 3.数据结构: Map 和 Set 的区别缓存方面,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。
- http1.1 中新增了 host 字段,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。
- http1.1 相对于 http1.0 还新增了很多请求方法,如 PUT、HEAD、OPTIONS 等。
# HTTP 1.1 和 HTTP 2.0 的区别
- 二进制协议:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。
- **多路复用:**HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。
- **数据流:**HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。
- **头信息压缩:**HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
- **服务器推送:**HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。
# HTTP3.0的特点
HTTP/3基于UDP协议实现了类似于TCP的多路复用数据流、传输可靠性等功能,这套功能被称为QUIC协议。
- 流量控制、传输可靠性功能:QUIC在UDP的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些TCP中的特性。
- 集成TLS加密功能:目前QUIC使用TLS1.3,减少了握手所花费的RTT数。
- 多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了TCP的队头阻塞问题。
- 快速握手:由于基于UDP,可以实现使用0 ~ 1个RTT来建立连接。
# 2.call() 和 apply() bind()
它们的作用一模一样,区别仅在于传入参数的形式的不同。
- apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
- call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
(1)call 函数的实现步骤:
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 处理传入的参数,截取第一个参数后的所有参数。
- 将函数作为上下文对象的一个属性。
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性。
- 返回结果。
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
(2)apply 函数的实现步骤:
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
- 将函数作为上下文对象的一个属性。
- 判断参数值是否传入
- 使用上下文对象来调用这个方法,并保存返回结果。
- 删除刚才新增的属性
- 返回结果
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
(3)bind 函数的实现步骤:
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
- 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
# 3.数据结构: Map 和 Set 的区别
# Map
Map
对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数Map
可以接受一个数组作为参数。
Map和Object的区别
- 一个
Object
的键只能是字符串或者Symbols
,但一个Map
的键可以是任意值。 Map
中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。Map
的键值对个数可以从 size 属性获取,而Object
的键值对个数只能手动计算。Object
都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
Map对象的属性
- size:返回Map对象中所包含的键值对个数
Map对象的方法
- set(key, val): 向Map中添加新元素
- get(key): 通过键值查找特定的数值并返回
- has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false
- delete(key): 通过键值从Map中移除对应的数据
- clear(): 将这个Map中的所有元素删除
map与其他数据结构的互相转换
Map
与对象的互换
const obj = {}
const map = new Map([['a', 111], ['b', 222]])
for(let [key,value] of map) {
obj[key] = value
}
console.log(obj) // {a:111, b: 222}
JSON
字符串要转换成Map
可以先利用JSON.parse()转换成数组或者对象,然后再转换即可。
# Set
Set
对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
本身是一个构造函数,用来生成Set
数据结构。Set
函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
Set中的特殊值
Set
对象存储的值总是唯一的,所以需要判断两个值是否恒等。有几个特殊值需要特殊对待:
- +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复
- undefined 与 undefined 是恒等的,所以不重复
- NaN 与 NaN 是不恒等的,但是在 Set 中认为NaN与NaN相等,所有只能存在一个,不重复。
Set实例对象的属性
- size:返回Set实例的成员总数。
Set实例对象的方法
add(value)
:添加某个值,返回 Set 结构本身(可以链式调用)。delete(value)
:删除某个值,删除成功返回true
,否则返回false
。has(value)
:返回一个布尔值,表示该值是否为Set的成员。clear()
:清除所有成员,没有返回值。
遍历方法
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每个成员。
由于Set
结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
Set 对象作用
- 数组去重(利用扩展运算符)
const mySet = new Set([1, 2, 3, 4, 4])
[...mySet] // [1, 2, 3, 4]
- 合并两个set对象
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let union = new Set([...a, ...b]) // {1, 2, 3, 4}
- 交集
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let intersect = new Set([...a].filter(x => b.has(x))) // {2, 3} 利用数组的filter方法
- 差集
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let difference = new Set([...a].filter(x => !b.has(x))) // {1}
# 二者区别
综上所述,主要有一下几个区别:
1.Map是键值对,Set是值的集合,当然键和值可以是任何的值;
2.Map可以通过get方法获取值,而set不能因为它只有值;
3.都能通过迭代器进行for...of遍历;
4.Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储
5.map和set都是stl中的关联容器,map以键值对的形式存储,key=>value组成pair,是一组映射关系。set只有值,可以认为只有一个数据,并且set中元素不可以重复且自动排序。
# 1/22 每日一题
- JavaScript中对象继承的方式有哪些
- 实现脚本异步加载方法的关键字有哪些
- 什么是JS事件循环,事件循环机制是什么
- 力扣第20题有效的括号
# 1. JavaScript中对象继承的方式有哪些?
原型式继承
// 原型式继承 // Object()可以理解为对传入的对象进行一个浅复制 let person = { name: 'szj', friends: ['zy', 'wjr', 'ghk'] } let me = Object(person); me.friends.push('ssss') console.log(me); console.log(person) //{ name: 'szj', friends: [ 'zy', 'wjr', 'ghk', 'ssss' ] } //{ name: 'szj', friends: [ 'zy', 'wjr', 'ghk', 'ssss' ] }
寄生式继承
// 寄生继承 // 像是对原型继承的另一种升级 , 采用了工厂模式的方法 增加了对象的方法 let jisheng = { name : 'sss', friends: ['ss' , 'www'] } const js = (obj) =>{ const clone = Object(obj); clone.sayHi = function (){ console.log(clone.name); } return clone } const m = js(jisheng); m.sayHi(); // sss
组合继承
// 组合继承 // 原型链 + 盗用构造函数 function Super(name){ this.name = name; this.friends = ['szj' , 'zy']; } Super.prototype.sayName = function (){ console.log(this.name); } function Sub(name){ // 盗用构造函数 Super.call(this , name); } // 原型链继承 Sub.prototype = new Super; const bob = new Sub('szj'); bob.sayName() console.log(bob.friends); // szj // [ 'szj', 'zy' ]
寄生式组合继承
// 寄生式组合继承 // 最佳实践!! 相比于组合继承 减少了构造函数调用的次数 function Superr (name){ this.name = name; this.friends = ['ss' , 'www' , 'ppp']; } Superr.prototype.say = function (){ console.log(this.name); } function Subb(name){ Superr.call(this , name) } function inheritPrototype (sub , superr) { const prototype = Object(superr.prototype); // 别忘了原型对象的指向 prototype.constructor = sub; sub.prototype = prototype; } inheritPrototype(Subb , Superr); const zy = new Subb('zy'); zy.say() console.log(zy.friends); // zy // [ 'szj', 'zy' ]
# 2. 实现异步加载方法的关键字有哪些?
- defer
- async
- 总结:
- 两者都是用来异步加载外部脚本的关键字
- defer 加载完成后 页面渲染后执行
- async 加载完成后 立即执行
- 多个 defer 按顺序依次执行 , 多个async执行顺序不定
# 3. 什么是JS的事件循环 , 事件循环机制是什么?
如图: JS是单线程的 , 在代码执行时,会将代码压入执行栈中保证函数的有序执行 , 遇到异步任务会将 任务抛给webapi进行处理 , 处理之后将回调函数推到任务队列(宏任务队列 , 微任务队列) , 当执行栈为空时 , 先执行微任务队列中的函数 , 如果需要渲染页面 , 则会渲染页面 , 最后执行宏任务队列中的函数。
Event Loop 执行顺序如下所示:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码
宏任务
- 微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
- 宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。
# 4 leetcode 20 有效的括号 (opens new window)
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
const stack = [];
for(let i = 0 ; i < s.length ; i++){
const c = s[i];
if(c === '{' || c === '[' || c === '('){
stack.push(c)
}else{
const m = stack[stack.length - 1]
if((m === '{' && c === '}') || (m==='[' && c === ']') || (m === '(' && c ===')')){
stack.pop();
}else{
return false
}
}
}
return stack.length === 0 ? true : false
};
var isValid = function(s) {
let map = new Map([
['(', ')'],
['{', '}'],
['[', ']'],
]);
const stack = [];
for (let i = 0; i < s.length; i++) {
if (map.has(s[i])) {
// 找到左括号 入栈
stack.push(s[i]);
} else if (map.get(stack[stack.length - 1]) !== s[i]) {
// 左右括号不匹配 字符串无效
return false;
} else {
// 找到匹配得右括号 弹栈
stack.pop();
}
}
return !stack.length;
};
# 1/23 每日一题
- let、const、var区别
- 数组去重的方法有哪些?
- vue双向绑定的原理
- 力扣第70题 爬楼梯
# 1. let、const、var区别
能用const的情况尽量使用const,其他情况下大多数使用let,避免使用var
- 作用域
- let 、 const 为块级作用域
- var为函数作用域
- 是否可以重复声明
- let var 可以重复声明
- const 不可以重复声明
- 变量提升
- var 具有变量提升
- 暂时性死区
- let 、const 具有暂时性死区 , 未声明之后不能使用
- 给全局添加属性
- var 可以给全局添加属性 let const 不会
- 初始值设置
- var let 声明时可以不设置初始值
- const 必须设置初始值
- 指针的指向
- let 可以修改指针的指向(重新赋值) , const 不能够修改指针的指向不可以重新赋值,但是引用数据类型的属性可以改变
区别 | var | let | const |
---|---|---|---|
是否有块级作用域 | × | ✔️ | ✔️ |
是否存在变量提升 | ✔️ | × | × |
是否添加全局属性 | ✔️ | × | × |
能否重复声明变量 | ✔️ | × | × |
是否存在暂时性死区 | × | ✔️ | ✔️ |
是否必须设置初始值 | × | × | ✔️ |
能否改变指针指向 | ✔️ | ✔️ | × |
# 2. 数组去重的方法有哪些?
# 1. 数组元素比较型
双层
for
循环// 双层for循环 // 前一个跟之后所有进行比较 , 重复了删除掉 function uniq(arr){ for(let i = 0 ; i < arr.length - 1 ; i++){ for(let j = i + 1 ; j < arr.length; j++){ if(arr[i] === arr[j]){ arr.splice(i , 1); // 删除后下表移动到原位置 j--; } } } return arr; }
排序后 相邻位置进行比较
// 排序进行后进行相邻比较 function sortQ(arr){ // 排序后 // 没参数 如果没有指明 compareFunction ,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。 arr.sort(); for(let i = 0 ; i < arr.length - 1 ; i++){ if(arr[i] === arr[i + 1]){ arr.splice(i , 1); i--; } } }
# 2.查找元素位置型
indexOf
查找元素并返回其第一个索引值这个方法有点秀哦!利用
indexOf
API的特性,且效率也是最佳!function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(arr.indexOf(arr[i]) === i){ res.push(arr[i]) } } return res; }
findIndex
返回数组中第一个满足测试函数的元素的索引function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(arr.findIndex((item) => item === arr[i]) === i ){ res.push(arr[i]); } } return res; }
# 3. 查找元素存在型
includes
// 查找元素存在型 function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(!res.includes(arr[i])){ res.push(arr[i]) } } return res; }
# 4. 利用数据结构类型
set
// set function uniq(arr){ return [...new Set(arr)] }
map
function uniq(arr){ const map = new Map(); arr.forEach(item =>{ map.set(item, true) }) // 返回键值 Object.keys(key) return[...map.keys()] }
# 5. 总结
在简单的测试用例大概 2000 万条数据下,indexOf
的方案速度相对最快
# 3. vue双向绑定的原理
- Mvc 模式 到 mvvm模式 的转变
Mvc 模式 controler 层要大量的控制dom
Mvvm 模式 是真正做到了数据与视图的分离, view 和 model 改变时 , vm层自动进行数据和视图的同步
vue.js 采用数据劫持结合发布者-订阅者模式的方式 , 通过Object.defineProperty()来劫持各个属性的setter、getter,在数据监听时发布消息给订阅者 , 触发响应的监听回调
发布 订阅者模式让双向绑定更有效率(一对多)
实现一个数据监听器 Observer
- 核心是 Object.defineProperty() , 将Observe的数据对象进行递归遍历 , 包括子属性的对象加上setter getter方法 , 赋值时就会调用setter方法,就监听到了数据变化
- 通知订阅者
实现Compile
- 解析模板的指令 , 将模板中的变量替换成数据 ,
- 初始化页面渲染 ,
- 并绑定更新函数 , 添加监听数据的订阅者 , 一但数据有变化 , 更新视图 --绑定更新函数
实现watcher (解析 compile 和 observe 的桥梁)
- 实例化在订阅者添加自己
- 自己有一个update()方法 --添加订阅者
- 待属性变动 , 接受通知,调用自身的update() , 并触发compile中的回调 --》更新视图
整合形成一个mvvm
# 4.70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com) (opens new window)
和斐波那契数列问题异曲同工~
很easy!
记忆化递归
理解记忆化递归 为自己的面试加分!
- 使用数组存储中间结果;
- 中间结果如果存在,则不要重复使用递归式进行计算!
由于递归太耗时,可以用记忆化递归避免重复的计算。 解题过程: 1.先对n为0这种特殊情况进行处理,然后n为1和2时直接return即可 2.memo数组:存储中间结果,避免重复计算 3.接下来就是判断memo[n]是否存在,如果计算过即存在,直接返回,无需重复计算;若不存在,则进行递归计算,为前两个之和。 代码
const memo = []
var climbStairs = function(n) {
if(n === 0) return 1
if(n <= 3) return n
// 记忆化递归 避免重复计算
if(memo[n]) {
return memo[n]
} else {
memo[n] = climbStairs(n-1) + climbStairs(n-2)
}
return memo[n]
}
经典动归问题
分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
爬上 n-1 阶楼梯的方法数量
爬上 n-2 阶楼梯的方法数量
动态规划的转移方程为:dp[i] = dp[i - 1] + dp[i - 2];
简单地使用动归求解(这里没必要使用递归这种时间复杂度较高的方法,除非用了记忆化递归~)
var climbStairs = function(n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for(let i = 2 ; i <= n ; i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n]
};
时间复杂度与空间复杂度都为O(N)
- 使用滚动数组(空间复杂度为1)实现动态规划(而不是使用空间复杂度为N的递归)
- 这也是官方题解的第一种方法(其他数学方法我退缩了XD)
- 这也是官方题解的第一种方法(其他数学方法我退缩了XD)
var climbStairs = function(n) {
let p = 0, q = 1, r = 1;// 初始化dp[0] dp[1] dp[2]
for (let i = 2; i <= n; i++) {
// 利用滚动数组达到O(1)空间复杂度的动归
p = q;
q = r;
r = p + q;
}
return r;
};
# 1/24 每日一题
1.字面量创建对象和 new 创建对象有什么区别,new 内部都实现了什么, 手写一个 new 2.
==
和===
有什么区别 3.在 JS 中为什么 0.2+0.1>0.3? 4.那为什么 0.2+0.3=0.5 呢?
# 1.字面量创建对象和 new 创建对象有什么区别,
- 字面量
let obj = {}
:- 创建对象更简单
- 方便阅读
- 不需要作用域解析
- 速度更快
# 1‘.new 内部都实现了什么, 手写一个 new
红宝书权威解释
更细致的内容之前我有总结过一篇,相当于是手写new Object
吧——
JS小知识 new关键字都做了什么? - 掘金 (juejin.cn) (opens new window)
- 【1】在内存中创建一个新对象
- 【2】使新对象的
__proto__
指向原函数的 prototype - 【3】改变 this 指向(指向新的 obj)
- 【4】执行构造函数,给新对象添加属性
- 【5】执行结果保存起来作为 result 并判断执行函数的结果是不是 null 或 Undefined
- 如果是则返回之前的新对象,
- 如果不是则返回 result
手写result
function myNew(fn, ...args) {
let obj = {}// 创建一个空对象
obj.__proto__ = fn.prototype// 使空对象的隐式原型指向原函数的显式原型
let result = fn.apply(obj, args)// this 指向 obj
return result instanceof Object ? result : obj// 返回创建的对象
}
# 2.==
和===
有什么区别
===是严格意义上的相等,会比较两边的数据类型和值大小 数据类型不同返回 false 数据类型相同,但值大小不同,返回 false
==是非严格意义上的相等, 两边类型相同,比较大小 两边类型不同,根据下方的规则,再进一步进行比较。
Null == Undefined ->true
String == Number ->先将 String 转为 Number,再比较大小
Boolean == Number ->现将 Boolean 转为 Number,再进行比较
Object == String,Number,Symbol -> Object 转化为原始类型
# 对象的强制类型转换流程总结
【1】调用valueOf()方法,是原始值类型就返回,不是就继续下一步
【2】调用toString()方式,是原始值类型就返回,不是就继续下一步
【3】调用Number,是原始值就返回,不是就报类型错误(也只有undefined大神会这样了😂
作者:敲代码的小提琴手 链接:https://juejin.cn/post/7022837573059870727 来源:稀土掘金
可以看看我之前总结的一篇隐式类型转换的文章
由一道面试题引入的对JavaScript隐式转换的学习 - 掘金 (juejin.cn) (opens new window)
红宝书这里的内容
# 3.在 JS 中为什么 0.2+0.1>0.3?
因为在 JS 中,浮点数是使用 64 位固定长度来表示的,其中——
- 1 位表示符号位
- 11 位 用来表示指数位
- 剩下的 52 位尾数位。
由于只有 52 位表示尾数位。 而 0.1 转为二进制是一个无限循环数 0.0001100110011001100......(1100 循环)
小数的十进制转二进制方法:
https://jingyan.baidu.com/article/425e69e6e93ca9be15fc1626.html 注意:小数的十进制转二进制的方法是和整数不一样的
由于只能存储 52 位尾数位,所以会出现精度缺失——
比如:把0.1 存到内存中,再取出来转换成十进制就不是原来的 0.1 了,就变成了 0.100000000000000005551115123126
而为什么 02+0.1 是因为
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010(0.1) + 0.0011001100110011001100110011001100110011001100110011010(0.2) = 0.0100110011001100110011001100110011001100110011001100111
// 结果转成十进制正好是 0.30000000000000004
# 4.那为什么 0.2+0.3=0.5 呢?
// 0.2 和 0.3 都转化为二进制后再进行计算
0.001100110011001100110011001100110011001100110011001101 + 0.0100110011001100110011001100110011001100110011001101 = 0.10000000000000000000000000000000000000000000000000001
//尾数为大于 52 位
// 而实际取值只取 52 位尾数位,就变成了
0.1000000000000000000000000000000000000000000000000000
0.2转化为二进制
+0.3转换为二进制
的结果恰巧前 52 位尾数都是 0,截取后恰好是 0.1000000000000000000000000000000000000000000000000000=0.1(2) 也就是 0.5(10)
# 5.为什么 console.log(0.1) 打印得到 0.1 呢
那既然 0.1 不是 0.1 了,为什么 console.log(0.1) 打印得到 0.1 呢?
在 console.log 的时候会——
- 将二进制转换为十进制
- 再将十进制转为字符串的形式
在转 换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串
# 1.25 每日一题
1、三栏布局,有多少种?思路和代码 2、给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 输入:digits = [1,2,3] 输出:[1,2,4] 3、css中定位有多少个值,每个值有什么特点 4、对BFC的理解,如何创建BFC
# 1、三栏布局,有多少种?思路和代码
重点是 圣杯布局 两翼布局
三栏布局一般指的是页面中一共有三栏,左右两栏宽度固定,中间自适应的布局,三栏布局的具体实现:
- 利用绝对定位,左右两栏设置为绝对定位,中间设置对应方向大小的 margin 的值。
.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 100px;
height: 100px;
background: tomato;
}
.right {
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 100px;
background: gold;
}
.center {
margin-left: 100px;
margin-right: 200px;
height: 100px;
background: lightgreen;
}
- 利用 flex 布局,左右两栏设置固定大小,中间一栏设置为 flex:1。
.outer {
display: flex;
height: 100px;
}
.left {
width: 100px;
background: tomato;
}
.right {
width: 100px;
background: gold;
}
.center {
flex: 1;
background: lightgreen;
}
- 利用浮动,左右两栏设置固定大小,并设置对应方向的浮动。中间一栏设置左右两个方向的 margin 值,注意这种方式**,中间一栏必须放到最后:**
.outer {
height: 100px;
}
.left {
float: left;
width: 100px;
height: 100px;
background: tomato;
}
.right {
float: right;
width: 200px;
height: 100px;
background: gold;
}
.center {
height: 100px;
margin-left: 100px;
margin-right: 200px;
background: lightgreen;
}
- ==圣杯布局==,利用浮动和负边距来实现。父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。
.outer {
height: 100px;
padding-left: 100px;
padding-right: 200px;
}
.left {
position: relative;
left: -100px;
float: left;
margin-left: -100%;
width: 100px;
height: 100px;
background: tomato;
}
.right {
position: relative;
left: 200px;
float: right;
margin-left: -200px;
width: 200px;
height: 100px;
background: gold;
}
.center {
float: left;
width: 100%;
height: 100px;
background: lightgreen;
}
- ==双飞翼布局==,双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。
.outer {
height: 100px;
}
.left {
float: left;
margin-left: -100%;
width: 100px;
height: 100px;
background: tomato;
}
.right {
float: left;
margin-left: -200px;
width: 200px;
height: 100px;
background: gold;
}
.wrapper {
float: left;
width: 100%;
height: 100px;
background: lightgreen;
}
.center {
margin-left: 100px;
margin-right: 200px;
height: 100px;
}
# 2、css中定位有多少个值,每个值有什么特点
position 有以下属性值:
属性值 | 概述 |
---|---|
absolute | 生成绝对定位的元素,相对于 static 定位以外的一个父元素进行定位。元素的位置通过 left、top、right、bottom 属性进行规定。元素会被移除文档流 |
relative | 生成相对定位的元素,相对于其原来的位置进行定位。元素的位置通过 left、top、right、bottom 属性进行规定。元素会被移除文档流 |
fixed | 生成绝对定位的元素,指定元素相对于屏幕视⼝(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变,⽐如回到顶部的按钮⼀般都是⽤此定位⽅式。 |
static | 默认值,没有定位,元素出现在正常的文档流中,会忽略 top, bottom, left, right 或者 z-index 声明,块级元素从上往下纵向排布,⾏级元素从左向右排列。 |
inherit | 规定从父元素继承 position 属性的值 |
前面三者的定位方式如下:
- relative:元素的定位永远是相对于元素自身位置的,和其他元素没关系,也不会影响其他元素。
fixed:元素的定位是相对于 window (或者 iframe)边界的,和其他元素没有关系。但是它具有破坏性,会导致其他元素位置的变化(没脱离文档流还随心随遇地放置在某个位置!)。
absolute:元素的定位相对于前两者要复杂许多。如果为 absolute 设置了 top、left,浏览器会根据什么去确定它的纵向和横向的偏移量呢?答案是浏览器会递归查找该元素的所有父元素,如果找到一个设置了position:relative/absolute/fixed
的元素,就以该元素为基准定位,如果没找到,就以浏览器边界定位。如下两个图所示:
这里真是学习到了啊!
# 3、什么是 BFC?如何创建BFC?BFC有什么使用场景?
- Block formatting contexts(块级格式化上下文):首先 BFC 是一个独立的布局环境,BFC 中元素的布局是不受外界影响的。
- 本质上是指 盒子内部的元素不会影响外部元素的一个布局
- 如何创建一个 BFC
- float 的值不为 none
- position 的值不为 static 或者 relative
- display 的值为 table-cell、table-caption、inline-block、flex、inline-flex 中的一个
- overflow 的值不为 visible
- BFC 的使用场景
- 使用 BFC 来防止外边距折叠
- 使用 BFC 来包含浮动,解决容器高度塌陷的问题
- 使用 BFC 来防止文字环绕
- 在多列布局中使用 BFC,解决最后一列被挤到下一行的问题
# 4、给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 输入:digits = [1,2,3] 输出:[1,2,4]
var plusOne = function(digits) {
const n = digits.length;
for (let i = n - 1; i >= 0; i--) {
digits[i]++;
digits[i] = digits[i] % 10;
if (digits[i] !== 0) {
return digits;// 这一位没有进位就可以直接返回结果了~
}
}
digits.unshift(1);// 在最高位前面还要再进一位~
return digits;
};
# 1.26 每日一题
1.你了解cdn吗?说一下cdn 2.Https是的加密策略讲一下 3.什么是闭包,闭包起什么作用 4.Vue中组件间传值有哪些方法呢? 5.算法题 最大回文子串
# 1.你了解cdn吗?说一下cdn
CDN也叫内容分发网络技术 , 目的是为了提高传输内容的传输速度和稳定性,避开网络上的拥塞和不稳定的节点 , 寻找最近的网络节点获取资源
cdn上的内容分为静态和动态内容
- 静态:提前备份到cdn服务器
- 动态:可能会在cdn服务器实现一些接口
如何转发:任播 -- 服务器对外拥有一个相同的ip地址 , 请求会被距离最近的cdn服务器接受
# 2.Https的加密策略
我也很喜欢看技术蛋老师的视频👇哈哈
https 分为三种加密加密算法
- 摘要算法 : 输入一系列子串 输入长度一定 , 常见的MD5
- 非对称加密:公钥加密 私钥解密 (公钥会传输 私钥不会传输)
- 对称加密:用同一个密钥加密 并且解密 异或的思想
拓展:
https的传输过程(4次握手)
- 客 : 发送协议的版本 可以支持的加密算法 以及一个随机数
- 服 : 确定加密算法 , 发送公钥 , 发送数字证书 以及一个随机数
- 客 : 将公钥加密后的随机数发送
- 服 : 将随机数用私钥解密
- 至此 , 将 第一随机数 第二随机数 加公钥加密后的随机数 生成 会话密钥 (对称加密)
# 3.JS如何进行垃圾回收的?如果回收出现循环引用会导致什么?
垃圾回收的策略
标记清理
- 将内存中的所有变量或者函数都进行标记 , 调用后去除标记 , 一段时间后 , 剩下还有标记的变量就会被回收
引用计数
- 跟踪每一个值被引用的次数 , 赋值加一 被覆盖减一 当为零的时候可以清理
循环引用
当出现两个值相互引用的现象 , 利用引用计数进行垃圾回收 , 会导致循环引用 , 永远不会释放 , 因此得手动解除引用
例如
obj1.a = obj2 obj2.a = obj1 //引用次数都为2 // 手动解除引用 obj1.a = null obj2.a = null
# 4.Vue中组件间传值有哪些方法呢?
每日一题 点滴进步~ - 掘金 (juejin.cn) (opens new window)
Vue我就不做拓展了 直接看这里就好——
- 父子组件
- 父组件传子组件
- props
- 子组件向父组件传值
- $emit()
- 子组件中定义一个方法
this.$eimt('父组件中的函数名' , 传的值的名字)
- 父组件中接受 @子组件中定义的名字 = ‘’函数名称‘ 直接以参数的形式接受
等,,,
我之前短暂用过一段时间的Vue 父子组件传值就都用的这个~
# 5.算法题 最大回文子串 (opens new window)
动态规划
贼经典的一题~
# 1.27 每日一题
1.如何判断两个链表是否相交?
2.对vue的响应式原理有了解么,可以简单的说一下么?
3.cookie是为了解决什么问题,session, localStorage, sessionStorage,这三者的区别是啥呢?
4.给你一棵树,树上的每条边权值都是1,求树上两个节点的距离,使得路径最大
# 1.如何判断两个链表是否相交?
# 面试题 02.07. 链表相交 (opens new window)
【JS 双指针】你走过我 来时的路✨ - 链表相交 - 力扣(LeetCode) (leetcode-cn.com) (opens new window)
var getIntersectionNode = function(headA, headB) {
let a = headA;
let b = headB;
if(a === null || b === null){
return null;
}
while(a !== b){
a = a===null ? headB : a.next;
b = b===null ? headA : b.next;
}
return a;
};
# 2.对vue的响应式原理有了解么,可以简单的说一下么?
Vue面试题我真顶不住啊朋友,回头再碰到Vue的面试题我就自己再找一个React的替换上好了~
不过这题我在字节二面被问到过,当时很尴尬地不会~(当然现在还是不会,,)
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
相关代码如下
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value); // 递归关键
// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
// 思考?如果Vue数据嵌套层级过深 >>性能会受影响
Object.defineProperty(data, key, {
get() {
console.log("获取值");
//需要做依赖收集过程 这里代码没写出来
return value;
},
set(newValue) {
if (newValue === value) return;
console.log("设置值");
//需要做派发更新过程 这里代码没写出来
value = newValue;
},
});
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
响应式数据原理详解 传送门 (opens new window)
作者:sword__song 链接:https://juejin.cn/post/7056032803531522078
# 3.【2】cookie是为了解决什么问题,session, localStorage, sessionStorage,这三者的区别是啥呢?
【2】代表第二次出现的题目,高频程度可见一斑咯~
这题又出现了一遍~再复习一下
- cookie (服务器设置 , 客户端储存 , 同源请求携带) 常用来识别用户 , 与session一起跟踪用户的状态。 最多储存4k
- session 保存在服务端 , 用来跟踪用户的状态 , 与cookie一起使用
- sessionStorage : HTML5 提供的一种浏览器本地存储的方法 , 类似于session , 代表了一次会话所保存的数据 , 页面关闭后失效
- loaclStorage:HTML5 提供的一种浏览器本地存储的方法 , 保存在本地 , 不删除就会永久的储存。
# 4.给你一棵树,树上的每条边权值都是1,求树上两个节点的距离,使得路径最大
额今天怎么两道力扣,
# 543. 二叉树的直径 (opens new window)
经典的‘求二叉树深度’的一个小变式
var diameterOfBinaryTree = function(root) {
let maxGap = 0;
const dfs = function(root) {
if (root === null) {
return 0;
}
let leftDepth = dfs(root.left);
let rightDepth = dfs(root.right);
// 与求深度那题的唯一区别——时刻计算直径~
maxGap = Math.max(maxGap, leftDepth + rightDepth)
return Math.max(leftDepth, rightDepth) + 1
}
dfs(root);
return maxGap;
};
# 剑指 Offer 55 - I. 二叉树的深度 (opens new window)
异曲同工的求深度
var maxDepth = function(root) {
let maxDepth = 0;// 用于记录最大深度
const dfs = function(root) {
if (root === null) {
return 0;
}
// “递”完了,每往上“归”一层,深度都加1咯~
let leftDepth = dfs(root.left);
let rightDepth = dfs(root.right);
// 更新深度~
maxDepth = Math.max(leftDepth, rightDepth) + 1;
return maxDepth;
}
dfs(root)
return maxDepth;
};
# 1.28 每日一题
1.力扣3 无重复字符的最长子串
2.css优先级是怎么计算的
3.typeof和instanceof的区别
4.v-if和v-show区别
# 2.css优先级是怎么计算的
css优先级是通过权重计算得到的(256进制~不过这个倒是了解即可)
选择器 权重 内联样式 1000 id选择器 100 类选择器、属性选择器、伪类选择器 10 标签选择器、伪元素选择器 1 另外 !inportant 优先级最高!!
# 3.typeof和instanceof的区别
相同点:两者都是操作符 用来判断类型
不同点
typeof: string number symbol bigInt boolean function object , 可以判断七种类型 只能判断对象 , 无法判断哪种对象
也就是只能判断出来基本数据类型~
instanceof : 可以判断 是哪种object 但是不能判断 原始数据类型
# 4.v-if和v-show区别
原理:v-if 是利用动态的向DOM树内添加或者删除DOM , v-show 是通过设置DOM元素的display属性控制显隐
编译过程:v-if 切换有一个局部编译/卸载的过程 , 切换过程中合适的销毁和重建内部的事件监听和子组件 , v-show只是进行一个css样式的切换
编译条件:v-if 是惰性的 , 当初是条件为假 , 不会进行一个编译 , 只有到条件为真的时候才开始真正的编译 , v-show 无论条件是否为真都会进行一个编译
性能消耗:v-if 涉及到编译卸载的过程 比较高的切换消耗, v-show 有比较高的初始渲染消耗
使用场景:频繁切换用v-show ,条件不大可能改变用v-if
作者:sword__song 链接:https://juejin.cn/post/7056032803531522078
# 1.29 每日一题
- null和undefined的区别
- 判断this指向的几种方法
- vue/React中key的作用,为什么不建议用index作为key
- promise.all实现
# 1. null和undefined的区别
一张 形象的图~
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
- undefined 代表的含义是**未定义 **常常是变量声明并没有赋值
- null 代表的含义是空对象 常用来赋值给一个可能返回对象的变量/销毁无用的对象
一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
# 2. 【2】判断this指向的几种方法
其实this指向的问题情况并不多,但是它在不同的执行条件下可能会绑定(指向)不同的对象,如果想要再深入了解,可以看一下coderwhy的这篇文章 (opens new window)
在 js 中一般理解就是谁调用这个 this 就指向谁
普通函数中:this->window
定时器中:this->window
构造函数中:this->当前实例化的对象
事件处理函数中:this->事件触发对象
call、apply、bind this->指定this
- 这里存疑~
根据箭头函数与否&构造函数划分——
- 箭头函数
- this指向为箭头函数外部的执行上下文
- 非箭头函数
- this指向该函数运行时的函数的执行上下文
- 构造函数
- this指向(构造的)新(实例)对象
# 3. vue/React中key的作用,为什么不建议用index作为key
官方文档中的阐述:
列表 & Key – React (docschina.org) (opens new window)
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key:
const todoItems = todos.map((todo) => <li key={todo.id}> {todo.text} </li> );
当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:
const todoItems = todos.map((todo, index) => // Only do this if items have no stable IDs <li key={index}> {todo.text} </li> );
如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响 (opens new window)这一篇文章。如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的 (opens new window)可以参考。
另外推荐这篇讲述diff算法与虚拟DOM渲染的文章——15张图,20分钟吃透Diff算法核心原理,我说的!!! (opens new window) 对下面👇的内容讲得比较通俗易懂且深入原理~
在子元素列表末尾新增元素时,更新开销比较小。比如:
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React 会先匹配两个
<li>first</li>
对应的树,然后匹配第二个元素<li>second</li>
对应的树,最后插入第三个元素的<li>third</li>
树。如果只是简单的将新增元素插入到表头,那么更新开销会比较大。比如:
<ul> <li>Duke</li> <li>Villanova</li> </ul> <ul> <li>Connecticut</li> <li>Duke</li> <li>Villanova</li> </ul>
React 并不会意识到应该保留
<li>Duke</li>
和<li>Villanova</li>
,而是会重建每一个子元素。这种情况会带来性能问题。为了解决上述问题,React 引入了
key
属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下示例在新增key
之后,使得树的转换效率得以提高:<ul> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul> <ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul>
现在 React 知道只有带着
'2014'
key 的元素是新元素,带着'2015'
以及'2016'
key 的元素仅仅移动了。实际开发中,编写一个 key 并不困难。你要展现的元素可能已经有了一个唯一 ID,于是 key 可以直接从你的数据中提取:
<li key={item.id}>{item.name}</li>
当以上情况不成立时,你可以新增一个 ID 字段到你的模型中,或者利用一部分内容作为哈希值来生成一个 key。这个 key 不需要全局唯一,但在列表中需要保持唯一。
最后,你也可以使用元素在数组中的下标index作为 key。这个策略在元素不进行重新排序时比较合适,如果有顺序修改,diff 就会变慢。
当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用(这是根据Diff算法做出的决定),如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改,会出现无法预期的变动。
上面是官方文档中的阐述,但是我们不鼓励使用indx作为key,正如官方文档最后一句说的,如果元素的顺序有变,则根据Diff算法判断到的“变动的虚拟DOM节点”是有误的! 需要重绘一大片区域的节点!—— 建议看一下这篇中的图:15张图,20分钟吃透Diff算法核心原理,我说的!!! (opens new window)
# 4. promise.all实现
1) 核心思路
- 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
- 这个方法返回一个新的 promise 对象,
- 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
- 参数所有回调成功才是成功,返回值数组与参数顺序一致
- 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
2)实现代码
一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if(!Array.isArray(promises)){
throw new TypeError(`argument must be a array`)
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value=>{
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter == promiseNum) {
return resolve(resolvedResult)
}
},error=>{
return reject(error)
})
}
})
}
// test
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1)
}, 1000)
})
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(2)
}, 2000)
})
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(3)
}, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
console.log(res) // [3, 1, 2]
})
# 1.30 每日一题
1.水平垂直居中的方法 2.ES6有哪些新特性? 3.宏任务和微任务都有哪些?有什么区别? 4.力扣:1.两数之和
# 1.水平垂直居中的方法
【青训营】做面试题般回顾前端基础知识CSS篇 - 4 弹性布局与经典面试题CSS实现垂直居中 - 掘金 (juejin.cn) (opens new window)
利用flex布局有两种方法;
.father{ display:flex; } .son{ margin: auto; }
.father{ display: flex; justify-content: center;/* 水平居中 */ align-items: center;/* 垂直居中 */ }
利用绝对定位有两种方法;
.father{ position: relative; } .son{ position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; }
.father{ position: relative; } .son{ position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);/* 需要考虑浏览器兼容问题 */ /* 上面这行等价于下面两行代码,需要知道子盒子的宽高~ */ margin-top: -50px;/* 子盒子高度的一半50px */ margin-left: -100px; /* 子盒子宽度的一半100px */ }
还有一种利用行内元素特性做的居中
line-height:height; text-align:center;
# 2.ES6有哪些新特性?
引用阮老师的这本口碑爆棚的 ECMAScript 6 入门 (opens new window) 的目录👇
let声明变量和const声明常量,两个都有块级作用域
- ES5中的var是没有块级作用域的,并且var有变量提升
箭头函数
- ES6中的函数定义不再使用关键字function(),而是利用了()=>来进行定义
模板字符串
- 模板字符串是增强版的字符串,用反引号(`)标识,可以当作普通字符串使用,也可以用来定义多行字符串
解构赋值
- ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值
- 数组解构
- 对象解构
- ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值
for of循环
- for...of循环可以遍历——
- 数组
- Set和Map结构
- 某些类似数组的对象、对象
- 以及字符串
- for...of循环可以遍历——
import、export导入导出 ES6标准中,Js原生支持模块(module)。将JS代码分割成不同功能的小块进行模块化,将不同功能的代码分别写在不同文件中,各模块只需导出公共接口部分,然后通过模块的导入的方式可以在其他地方使用
set数据结构
- Set数据结构,类似数组。所有的数据都是唯一的,没有重复的值。它本身是一个构造函数
...
展开运算符- 可以将数组或对象里面的值展开;还可以将多个值收集为一个变量
修饰器 @
- decorator是一个函数,用来修改类甚至于是方法的行为。修饰器本质就是编译时执行的函数
class 类的继承
- ES6中不再像ES5一样使用原型链实现继承,而是引入Class这个概念
async、await
使用 async/await, 搭配promise,可以通过编写形似同步的代码来处理异步流程, 提高代码的简洁性和可读性
async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成
简单来说
如果await等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。 如果await等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
promise
- Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理、强大
Symbol BigInt …
- Symbol是一种基本类型。Symbol 通过调用symbol函数产生,它接收一个可选的名字参数,该函数返回的symbol是唯一的
Proxy代理
- 使用代理(Proxy)监听对象的操作,然后可以做一些相应的事情
# 3.宏任务和微任务都有哪些?有什么区别?
推荐文章 10分钟理解JS引擎的执行机制 - SegmentFault 思否 (opens new window)
# 1. 宏任务
- I/O
- setTimeout
- setInterval
- setImmediate
# 2. 微任务
- Promise
# 3. 区别
- 执行顺序
- 当执行栈为空时 , 先执行微任务 , 后执行宏任务
- 进程的切换为宏任务 , 线程的切换为微任务。
# 4.力扣:1.两数之和 (opens new window)
哈希表逆向思维
var twoSum = function(nums, target){
let map = new Map();
for(let i = 0; i < nums.length; i++){
if(map.get(target - nums[i]) >= 0){
return [i, map.get(target - nums[i])];
}
else{
map.set(nums[i], i);
}
}
}
# 2.7 每日一题
复工快乐!
1.ES6 和 commonjs 的区别
2.谈一谈下列两种写法的区别
// 第一种 promise.then((res) => { console.log('then:', res); }).catch((err) => { console.log('catch:', err); })
// 第二种 promise.then((res) => { console.log('then:', res); }, (err) => { console.log('catch:', err); })
3.在你开发的过程中,什么情况下会遇到跨域问题,你是怎么解决的?
4.
["1", "2", "3"].map(parseInt)
答案是多少?
# 1.ES6 和 Commonjs
的区别
Node.js 能在诞生后火到如此一塌糊涂,离不开它成熟的模块化实现,Node.js 的模块化是在
Commonjs
规范的基础上实现的。 那Commonjs
又是什么呢?在维基百科 (opens new window)上的定义:
Commonjs
是一个项目,其目标是为 JavaScript 在网页浏览器之外创建模块约定。创建这个项目的主要原因是当时缺乏普遍可接受形式的 JavaScript 脚本模块单元,模块在与运行JavaScript 脚本的常规网页浏览器所提供的不同的环境下可以重复使用。
好文推荐:前端科普系列-CommonJS:不是前端却革命了前端 - 知乎 (zhihu.com) (opens new window)
简单的区别——
Commonjs
模块输出的是值的拷贝,而 ES6 输出的值是值的引用Commonjs
是在运行时加载,是一个对象,ES6 是在编译时加载,是一个代码块Commonjs
的 this 指向当前模块,ES6 的 this 指向 undefined
# 2.then方法第二个参数&catch方法对promise抛出错误的捕捉
首先明确下:如果是promise内部报错 reject 抛出错误后,要看then方法的第二个参数存在与否——
then方法第二个参数&catch方法对promise抛出错误的捕捉
- 捕获上一个promise对象抛出的异常
- 相当于给前面一个then方法返回的promise 注册回调,可以捕获到前面then(上一个promise对象抛出的)没有被处理的异常
- (第二个参数不存在)捕获catch之前整条promise链路抛出的异常
- 仅为上一个promise 注册异常回调
谈一谈下列两种写法的区别
// 第一种
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
- catch 方法可以捕获到 catch 之前整条 promise 链路上所有抛出的异常。
- 这种是链式写法,使用catch,相当于给前面一个then方法返回的promise 注册回调,可以捕获到前面then没有被处理的异常。
// 第二种
promise.then((res) => {
console.log('then:', res);
}, (err) => {
console.log('catch:', err);
})
- then 方法的第二个参数捕获的异常依赖于上一个 Promise 对象的执行结果。
- 这种是回调函数写法,仅为上一个promise 注册异常回调。
# 3.在你开发的过程中,什么情况下会遇到跨域问题,你是怎么解决的?
简单来说就是:
1.JSONP 使用script标签向后台请求数据,只适用于get请求
2.CORS是让服务器端设置Access-Control-Allow-Origin(头部字段),这样浏览器就不会报跨域错误
3.反向代理,搭建一个自己的服务器,让自己的服务器请求数据,拿到数据之后再返回给我自己
下面是学长工作过程中的一些经验之谈
- API跨域可以通过服务器上nginx反向代理
- 本地webpack dev server可以设置 proxy,
- new Image, 设src 的时候,图片需要设置Cors cors需要后台配合设置HTTP响应头,如果请求不是简单请求(1. method:get,post,2. content-type:三种表单自带的content-type,3. 没有自定义的HTTP header),浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
- jsonp是浏览器会放过 img script标签引入资源的方式。所以可以通过后端返回一段执行js函数的脚本,将数据作为参数传入。然后在前端执行这段脚本。双方约定一个函数的名称。
- 联调的时候会需要跨域,线上前端站点域和后台接口不一致也需要跨域,开发时跨域可以通过代理服务器来转发请求,因为跨域本身是浏览器对请求的限制,常见的跨域处理还有JSONP和cors,jsonp是利用脚本资源请求本身就可以跨域的特性,通过与请求一起发送回调函数名,后台返回script脚本直接执行回调,但是由于资源请求是get类型,请求参数长度有限制,也不能进行post请求。cors需要后台配合设置HTTP响应头,如果请求不是简单请求(1. method:get,post,2. content-type:三种表单自带的content-type,3. 没有自定义的HTTP header),浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
- 同源策略只是浏览器客户端的防护机制,当发现非同源HTTP请求时会拦截响应,但服务器依然处理了这个请求。 服务器端不拦截,所以在同源服务器下做代理,可以实现跨域。
# 4.["1", "2", "3"].map(parseInt)
答案是多少?
神奇的答案XD
parseInt(string, radix)
(opens new window) 解析一个字符串并返回指定基数的十进制整数, radix
是2-36之间的整数,表示被解析字符串的基数。
但此处 map 传了 3 个 (element, index, array),我们重写parseInt
函数测试⼀下是否符合上⾯的规则。
这个重写parseInt函数的方式很新颖~
function parseInt(str, radix) {
return str+'-'+radix;
};
var a=["1", "2", "3"];
a.map(parseInt); // ["1-0", "2-1", "3-2"]
规定:radix范围2-32,string不能大于radix
因为⼆进制⾥⾯,没有数字3,导致出现超范围的radix赋值和不合法的进制解析,才会返回NaN 所以["1", "2", "3"].map(parseInt)
答案也就是:[1, NaN, NaN]
话说前两个为啥可以算出来我也不是很理解,回头仔细读一下MDN文档看看parseInt的定义
# 2.8 每日一题
1.svg 和 canvas 的区别?优缺点?
2.什么是 BFC?如何创建BFC?BFC有什么使用场景?
3.引起内存泄漏的操作有哪些?
4.实现一个sleep函数 - 字节实习二面原题
# 1.svg 和 canvas 的区别?优缺点?
HTML 5 Canvas vs. SVG (w3school.com.cn) (opens new window)
SVG——
- 是一种使用 XML 描述 2D 图形的语言。
- SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的。您可以为某个元素附加 JavaScript 事件处理器。
- 在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。
Canvas——
- 通过 JavaScript 来绘制 2D 图形。
- Canvas 是逐像素进行渲染的。
- 在 canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。
# Canvas
- 依赖分辨率
- 不支持事件处理器
- 弱的文本渲染能力
- 能够以 .png 或 .jpg 格式保存结果图像
- 最适合图像密集型的游戏,其中的许多对象会被频繁重绘
# SVG
- 不依赖分辨率
- 支持事件处理器
- 最适合带有大型渲染区域的应用程序(比如谷歌地图)
- 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
- 不适合游戏应用
其他一些重点👇
前者是矢量图,不失真,后者为位图(标量图);
前者为好多标签组成,后者只有一个标签;
前者适合大规模数据展示,后者适合小规模(适合带有大型渲染区域的应用程序,比如地图;但是如果复杂度高的话会影响效率,游戏之类的不合适~),东西太多耗内存;
# 2.不使用临时变量交换a,b的值
巧妙加减
let a = 1, b = 2;
a = a + b; // 1 + 2 = 3
b = a - b; // 3 - 2 = 1
a = a - b; // 3 - 1 = 2
利用异或运算
- a ^ a = 0
- 0 ^ a = a
let a = 1, b = 2;
a = a ^ b;
b = a ^ b; // (a ^ b) ^ b = a
a = a ^ b; // (a ^ b) ^ a = b
解构赋值
- JS特色~
[a, b] = [b, a]
# 3.引起内存泄漏的操作有哪些?
- 全局变量引起的内存泄漏
- 闭包引起的内存泄漏
- 被遗忘的定时器
- 未清理的 DOM 元素引用
- DOM 清空或删除时,事件未清除
# 4.实现一个sleep函数
/**
* 睡眠指定时间 返回promise实例
* @param {int} duration 睡眠时间, 毫秒
* @return {Promise Instance} Promise 实例
*
* @example
* sleep(100)
* => Promise {<pending>}
*
*/
const sleep = (duration) => new Promise((resolve) => {
// 写法1
setTimeout(() => {
resolve();
}, duration)
// 写法2
setTimeout(resolve, time)
});
sleep(1000).then(() => {console.log("sleep")});
# 2.9 每日一题
1、元素浮动后的变化
2、从输入url到页面加载完成发生了什么
3、数组扁平化的方法
4、封装一个ajax
# 1、元素浮动后的变化
浮动元素脱离当前文档流,父元素高度减少浮动元素高度(激活BFC),后面的元素顶上去
浮动元素宽度变成盒子实际宽度,不再占一行
后面元素的内容content会错开浮动元素实际宽度,不会被浮动元素覆盖
# 2、从输入url到页面加载完成发生了什么
简略版本 可以展开说很多内容
1、浏览器的地址栏输入URL并按下回车,符合HTTP协议则进入下一步,否则转到搜索引擎。
2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
3、DNS解析URL对应的IP。
4、根据IP建立TCP连接(三次握手)。
5、HTTP发起请求。
6、服务器处理请求,浏览器接收HTTP响应。
7、渲染页面,构建DOM树。
8、关闭TCP连接(四次挥手)。
# 3、数组扁平化的方法
- flat
- 转字符串
let tempArr = [1,2,3,[4,5],6];
let result = tempArr.join(',').split(',');
// ["1", "2", "3", "4", "5", "6"]
- reduce
- 从
arr
第一项开始(所以要给acc累加器加上初始值[]
) - 如果遇到的值为数组 则扔到递归调用栈里进行递归遍历 , 否则直接接到acc的尾部(使用
concat
)
- 从
const flatten = (arr) => {
return arr.reduce((acc, cur) => {
return acc.concat(Array.isArray(item) ? flatten(item) : item);
}, [])
}
- 扩展运算符
[].concat(...[1,2,3,[4,5],6])
// 多维数组
const flat = (arr) => {
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
# 4、封装一个ajax
function ajax(param){
try{
if(param!=null&&typeof param=="object"){
//创建ajax对象
if(window.XMLHttpRequest){
var xmlHttp=new XMLHttpRequest();
}else{
var xmlHttp=new ActiveXOject("Microsoft.XMLHTTP");
}
xmlHttp.onload=function(){
param.completed();
}
//设置接口地址和请求方式
if(param.type=="GET"&¶m.data!=undefined){
xmlHttp.open(param.type,param.url+"?"+param.data);
}else{
xmlHttp.open(param.type,param.url);
}
//设置数据的编码格式
if(param.contentType!=undefined&¶m.contentType!="formdata"){
switch(param.contentType){
case "urlencoded":
xmlHttp.setRequestHeader("content-type","application/x-www-form-urlencoded");
break;
case "json":
xmlHttp.setRequestHeader("content-type","application/json");
break;
}
}
//监控请求ajax
xmlHttp.onreadystatechange=function(){
if(xmlHttp.readyState==4&&xmlHttp.status==200){
var data=xmlHttp.responseText;
switch(param.dataType){
case "json":
data=JSON.parse(data);
break;
}
param.success(data);
}
}
//发送请求
if(param.type=="POST"&¶m.data!=undefined){
xmlHttp.send(param.data);
}else{
xmlHttp.send();
}
}else{
throw new Error("参数不正确");
}
}catch(e){
alert(e.message);
}
}
# 2.10-2.20停更两周
准备春招太忙碌了呜呜呜 下周开始
希望能进入心意的公司