前端面试题集每日一练Day18
问题先导
- 什么时候需要清除浮动?都有哪些方法?【css定位】
- 使用
clear
清除浮动的原理?【css定位】 - 对BFC的理解,如何创建BFC【css定位】
- 什么是margin重叠问题,如何解决?【css定位】
- 对原型、原型链的理解【js原型】
- 原型的修改和重写【js原型】
- 原型链的指向问题【js原型】
- 原型链的终点是什么【js原型】
- 如何获取对象中非原型链的属性【js原型】
- 说一下Vue的生命周期【Vue生命周期】
- Vue实例的子组件和父组件生命周期钩子执行顺序如何【Vue生命周期】
- 生命周期钩子函数created和mounted的区别【Vue生命周期】
- 一般在哪个生命周期获取异步数据【Vue生命周期】
- keep-alive的生命周期有哪些【Vue生命周期】
- 使用ES5和ES6实现函数求和【手写代码】
- 解析URL Params为对象【手写代码】
- 输出结果(Promise相关)【代码输出结果】
- 最长重复子数组【算法】
知识梳理
什么时候要清除浮动?有哪些方式?
浮动both
会让元素脱离文档流,当子元素均浮动,而父元素未设置高度时,父元素就会出现“高度坍塌”现象,这时候就可能影响与父元素同级的页面布局。
清除浮动的方式就是在父元素尾部增加一个空白元素,然后使用clear: both;
属性,当然也可以设置其他属性值。
<div class="outer">
<div class="inner">
I'm the inner box!
</div>
<div class="after"></div>
</div>
.outer {
margin: 0px auto;
width: 500px;
background-color: #dedede;
}
.inner {
width: 200px;
height: 200px;
background-color: #9ed0c4;
float: left;
}
.after {
clear: left;
}
没有末尾子元素的clear
属性,就看不到父元素了。
当然,我们也可以使用::after
伪元素来清除浮动,也就不必改动html
代码了。
.outer::after {
content: '';
clear: both;
display: block;
}
使用clear
清除浮动的原理?
使用clear属性清除浮动,其语法如下:
clear: none | left | right | both | inline-start | inline-end
如果单看字面意思,clear:left 是“清除左浮动”,clear:right 是“清除右浮动”,实际上,这种解释是有问题的,因为浮动一直还在,并没有清除。
clear
属性指定一个元素是否可以在它之前的浮动元素旁边,或者必须向下移动(清除浮动) 在它的下面。clear 属性适用于浮动和非浮动元素。
对BFC的理解,如何创建BFC?
BFC是块格式化上下文(Block Formatting Context)的缩写,它会创建一个特殊的区域,在这个区域中,只有block box
参与布局,布局方式决定了区域内的元素的定位和相关欢喜,不收到外界区域的布局影响。就像一个一个独立的盒子,内部与外界完全隔离开。
通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
触发BFC的几种场景:
- 根元素:body
- 元素设置为浮动布局
- 元素设置为绝对定位
- display为
block box
类型如:inline-block、table、flex等等 - overflow值为:hidden、auto、scroll
BFC的特点:
- 和文档流一样排列:垂直方向上,自上而下排列
- BFC中上下相邻的两个子容器
margin
会重叠 - 计算BFC高度时,需要计算浮动元素的高度
- BFC元素不会与浮动容器发生重叠,因为浮动容器本身就是一个BFC
- BFC内部元素不不受外界影响
- 每个元素的左margin和容器的左border相接触
利用BFC的特点,我们可以解决下面的问题:
-
解决某些情况下margin塌陷的问题(垂直方向的 margin 父子结构(或 兄弟结构 )是结合到一起的,他俩会取最大的那个值)
<div class="father"> <div class="son"></div> </div>
.father{ width: 200px; height: 200px; background-color: #f00; } .son{ margin-top: 20px; width: 50px; height: 50px; background-color: #000; }
我们发现子元素
margin
无效,或者说与父元素的margin
重叠了,这时可以通过设置父元素为BFC来解决这个问题。 -
解决子元素浮动时父元素高度塌陷的问题:子元素浮动时,父元素如果没有设置高度会产生高度塌陷,这个时候就可以将父元素设置为BFC来解决
-
创建自适应两栏布局
.left{ width: 100px; height: 200px; background: red; float: left; } .right{ height: 300px; background: blue; overflow: hidden; }
左侧设置
float:left
,右侧设置overflow: hidden
。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局。
参考:
什么是margin重叠问题?如何解决
两个块级元素上下相邻时,上外边距和下外边距可能会合并为一个外边距,看起来就是两个外边距重叠的效果,这就是margin重叠的问题,因为有时候我们并不希望两个块元素发生外边距重叠。
外边距重叠有两种情形:
- 相邻兄弟之间重叠
- 父子之间重叠
最常用的解决办法就是触发BFC(块格式化上下文)。
对于兄弟之间的重叠,可以设置底部元素为BFC
,如dsiplay: inline-block;
等等。
对于父子之间的重叠,可以将父元素设置为BFC
,如overflow: hiddeen;
等等,有时候将子元素设置为BFC也能解决:使用浮动、绝对定位、设置为行内盒元素等,或者将父元素增加一个border也可:border 1px solide transparent;
。
对原型、原型链的理解
在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数内部都有一个prototype
属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法,这就是property
属性就是我们常说显示原型,因为需要暴露出去用于实例共享(继承),因为是“显示”的,而有显示就有隐式,隐式原型一般是相对于实例来说的,实例的隐式原型实际上就是父类显示原型的引用,以前我们用__proto__
属性来表示隐式原型,不过现在一般用Object.getPrototypeOf(obj)
方法来获取。
总结就是:父类A的构造函数有一个prototype
显示原型属性,用于实例B的数据继承。而实例会保留父类的显示原型,在实例中我们称之为隐式原型,即:
A.prototype == Object.getPrototypeOf(B); // 父类A的显示原型等于实例B的隐式原型
还有一点值得注意的是,构造函数的原型会引用自身,也就是原型上的构造器就是构造函数本身:
A.prototype.constrictor == A
此外,子类的prototype
相当于父类的一个实例,因此有:
Object.getPrototypeOf(A.prototype) == AParent.protytype;
从这上面的公式出发,我们就可以从实例B层层推导出B的父级构造函数:
-
实例隐式原型得到父类隐式原型:
Object.getPrototypeOf(B)
得到A.prototype
。 -
父类的隐式原型的构造函数就得到了父类本身:
A.prototype.constrictor
得到A;从
Object.getPrototypeOf(A.prototype)
得到AParent.prototype
。 -
依次类推,层层向上遍历,就得到了一条链式引用关系,这就是实例B的原型链。
const A_prototype = Object.getPrototypeOf(B);
const AParent_prototype = Object.getPrototypeOf(A_prototype)
//...
原型的修改、重写
修改原型就像正常修改对象那样即可:
A.prototype.name = 'jinx';
A.prototype.getMax = function(a, b) {
return Math.max(a, b);
}
重写原型意为着obj.__proto__
被整个替换了,可以直接替换:
obj.__proto__ = proto;
或者使用Object.setPrototypeOf
函数来修改原型。
原型链的指向问题
参考案例:
p.__proto__ // Person.prototype
Person.prototype.__proto__ // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor // Person
原型链的终点是什么?
所有对象都继承自Object
构造函数,因此Object.prototype.__proto__
就是原型链的终点,即null
。
值得注意的是,Object实际上也是一个函数,所以Object instanceof Function
返回true
。
如何获取对象上中非原型链的属性?
使用for in
可以遍历原型链上的所有可迭代属性,但有时候我们不需要原型链上的属性,只需要自身属性,这个时候可以换成for of
遍历,当然,需要支持这个遍历语句的对象才行。
使用for in
中,我们在搭配Object.hasOwnProperty
就能判断出当前属性是否属于自身。
说一下Vue的生命周期
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
下图展示了实例的生命周期:
Vue的生命周期可以分为四个部分:创建、挂载、更新和销毁。每个阶段对应两个阶段前后钩子,因此,Vue实例生命周期共8个钩子可供使用:
-
创建阶段
beforeCreate
:创建前。Vue实例对象的一些基本准备工作,也就是数据监听、事件处理等都未开始。created
:创建后。实例已经创建完成,即数据已经实现监听、事件已经绑定,所有需要用到的实例配置都以处理结束,但还未挂载到对应的DOM上,也就是模板尚未编译。
-
挂载阶段
befooreMount
:挂载前。挂载前主要是已经完成了模板编译工作。mounted
:挂载后。创建vm.$el
并替换了el
项,完成了DOM的渲染工作(异步)。
-
更新阶段
更新阶段就是数据监听到了数据变化,触发虚拟节点更新以及渲染的过程
beforeUpdate
:更新前。数据变化并触发更新时调用。updated
:更新后,虚拟节点已经渲染结束,此时的DOM已经是最新的状态了,所以可以执行依赖DOM的操作,但是大多数情况下需要谨慎使用,避免循环更新数据。
-
销毁阶段
beforeDestroy
:销毁前。实例销毁前调用,此时,实例任然可用。destroyed
:销毁后,实例销毁结束,实例关联的数据监听以及事件绑定都会被移除,所有子实例也会被销毁。
需要注意的是,keep-alive
有独立的生命周期,分别为 activated
和 deactivated
。用 keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated
钩子函数,命中缓存渲染后会执行 activated
钩子函数。
参考:
Vue实例的子组件和父组件的生命周期钩子执行顺序
-
创建和挂载阶段
先创建父、子组件,再一起挂载。
- 父组件:beforeCreate、created、beforeMounted
- 子组件:beforeCreate、created、beforeMounrted
- 子组件:mounted
- 父组件:mounted
-
更新阶段
- 父组件:beforeUpdate
- 子组件:beforeUpdate
- 子组件:updated
- 父组件:updated
-
销毁过程
- 父组件:beforeDestroy
- 子组件:beforeDestroy
- 子组件:destroyed
- 父组件:destroyed
生命周期钩子created和mounted的区别
created
属于创建阶段,在实例创建结束之后调用,此时数据监听、相关事件绑定已完成,但dom未渲染。
而mounted
属于挂载阶段,此时dom已经完成了渲染工作。
- created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
- mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
一般在哪个生命周期请求异步数据
进行异步请求数据一般是实例已经创建结束,即created
钩子中调用,虽然beforeMount
、mounted
也可以进行调用异步请求,但SSR不支持 beforeMount 、mounted 钩子,放在created
中有助于一致性统一处理。
keep-alive中的生命周期有哪些?
keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
组件缓存到内存时触发deactivated
钩子,当被激活时又触发actived
钩子。
使用ES5和ES6求函数参数的和
很简单,直接看答案好了:
ES5:
function sum() {
var total = 0;
Array.prototype.forEach.call(arguments, function(val) {
total += (+val);
});
return total;
}
ES6:
function sum(...nums) {
return nums.reduce((total, val) => {
return total + val;
}, 0);
}
解析 URL Params 为对象
示例:
console.log(parseUrlParam('http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled'));
// { user: 'anonymous', id: [ 123, 456 ], city: '北京', enabled: true }
关键点:Url Params对象即?
后面的部分,并以&
作为分隔符存储键值对。其中,值需要使用decodeURIComponent
方法进行界面。
/**
* 解析Url Param
* @param {string} url
* @returns Object
*/
function parseUrlParam(url) {
const paramObj = {};
// 获取地址 ? 后面的参数
if(url && url.match(/.+\?(.+)$/)) {
const paramsStr = RegExp.$1;
const paramArr = paramsStr.split('&');
paramArr.forEach(item => {
let [key, value = ''] = item.split('=');
value = decodeURIComponent(value);
// 参数值的特殊处理
if(value == '') {
value = true;
}else if(!isNaN(+value)) {
value = parseFloat(value);
}
if(key) {
if(paramObj.hasOwnProperty(key)) {
paramObj[key] = [paramObj[key], value];
}else {
paramObj[key] = value;
}
}
});
}
return paramObj;
}
输出结果(Promise相关)
代码片段:
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
执行逻辑:
console.log(1); // 1.打印1
// 2.异步宏任务
setTimeout(() => {
// 9.打印2
console.log(2);
// 10.异步微任务。紧接着执行微任务队列
Promise.resolve().then(() => {
// 11.打印3
console.log(3)
});
});
// 3.执行异步函数
new Promise((resolve, reject) => {
// 4.打印4
console.log(4)
// 5.异步微任务
resolve(5)
}).then((data) => {
// 8.打印5。微任务执行结束,开始执行宏任务队列,即2
console.log(data);
})
// 6.异步宏任务。
setTimeout(() => {
// 12.打印6
console.log(6);
})
// 7.打印7。宏任务执行结束,开始执行微任务队列,即步骤5
console.log(7);
输出结果:
1
4
7
5
2
3
6
代码片段:
Promise.resolve().then(() => {
console.log('1');
throw 'Error';
}).then(() => {
console.log('2');
}).catch(() => {
console.log('3');
throw 'Error';
}).then(() => {
console.log('4');
}).catch(() => {
console.log('5');
}).then(() => {
console.log('6');
});
执行逻辑
// 1.进入异步微任务
Promise.resolve().then(() => {
// 2.打印1
console.log('1');
// 3.抛出错误,catche回调进入异步微任务队列
throw 'Error';
}).then(() => {
console.log('2');
}).catch(() => {
// 4.打印3
console.log('3');
// 5.抛出错误,catche回调进入异步微任务队列
throw 'Error';
}).then(() => {
console.log('4');
}).catch(() => {
// 6.打印5
console.log('5');
// 7.无返回值,默认返回成功回调
}).then(() => {
// 打印6
console.log('6');
});
代码片段
setTimeout(function () {
console.log(1);
}, 100);
new Promise(function (resolve) {
console.log(2);
resolve();
console.log(3);
}).then(function () {
console.log(4);
new Promise((resove, reject) => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 10);
})
});
console.log(7);
console.log(8);
**执行逻辑 **
// 1.异步宏任务
setTimeout(function () {
// 11.打印1
console.log(1);
}, 100);
// 2.执行Promise
new Promise(function (resolve) {
// 3.打印2
console.log(2);
// 4.进入异步微任务
resolve();
// 5.打印3
console.log(3);
}).then(function () {
// 7.打印4
console.log(4);
// 执行Promise
new Promise((resove, reject) => {
// 8.打印5
console.log(5);
// 9.异步宏任务,但时间比1短很多,所以会先执行
setTimeout(() => {
// 10.打印6
console.log(6);
}, 10);
})
});
// 6.打印7, 8。开始执行微任务
console.log(7);
console.log(8);
最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
如果使用暴力解法,伪代码如下:
ans = 0
for i in [0 .. A.length - 1]
for j in [0 .. B.length - 1]
k = 0
while (A[i+k] == B[j+k]) do # and i+k < A.length etc.
k += 1
end while
ans = max(ans, k)
end for
end for
暴力解法的最坏时间复杂度为 O(n^3)。
暴力解法中A[i]
与B[j]
多次比较,实际上是可以优化的,不妨设 A 数组为 [1, 2, 3],B 两数组为为 [1, 2, 4] ,那么在暴力解法中 A[2] 与 B[2] 被比较了三次。这三次比较分别是我们计算 A[0:] 与 B[0:] 最长公共前缀、 A[1:] 与 B[1:] 最长公共前缀以及 A[2:] 与 B[2:] 最长公共前缀时产生的。
我们希望优化这一过程,使得任意一对 A[i] 和 B[j] 都只被比较一次。这样我们自然而然想到利用这一次的比较结果。如果 A[i] == B[j],那么我们知道 A[i:] 与 B[j:] 的最长公共前缀为 A[i + 1:] 与 B[j + 1:] 的最长公共前缀的长度加一,否则我们知道 A[i:] 与 B[j:] 的最长公共前缀为零。
这样我们就可以提出动态规划的解法:令 dp[i][j] 表示 A[i:] 和 B[j:] 的最长公共前缀,那么答案即为所有 dp[i][j] 中的最大值。如果 A[i] == B[j],那么 dp[i][j] = dp[i + 1][j + 1] + 1,否则 dp[i][j] = 0。
考虑到这里 dp[i][j] 的值从 dp[i + 1][j + 1] 转移得到,所以我们需要倒过来,首先计算 dp[len(A) - 1][len(B) - 1],最后计算 dp[0][0]。
动态规划:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
n, m = len(A), len(B)
dp = [[0] * (m + 1) for _ in range(n + 1)]
ans = 0
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
dp[i][j] = dp[i + 1][j + 1] + 1 if A[i] == B[j] else 0
ans = max(ans, dp[i][j])
return ans
转载自:https://juejin.cn/post/6981856285406789645