likes
comments
collection
share

前端面试题集每日一练Day18

作者站长头像
站长
· 阅读数 3

问题先导

  • 什么时候需要清除浮动?都有哪些方法?【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的父级构造函数:

  1. 实例隐式原型得到父类隐式原型:Object.getPrototypeOf(B)得到A.prototype

  2. 父类的隐式原型的构造函数就得到了父类本身:A.prototype.constrictor得到A;

    Object.getPrototypeOf(A.prototype)得到AParent.prototype

  3. 依次类推,层层向上遍历,就得到了一条链式引用关系,这就是实例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 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

下图展示了实例的生命周期:

前端面试题集每日一练Day18

Vue的生命周期可以分为四个部分:创建、挂载、更新和销毁。每个阶段对应两个阶段前后钩子,因此,Vue实例生命周期共8个钩子可供使用:

  1. 创建阶段

    • beforeCreate:创建前。Vue实例对象的一些基本准备工作,也就是数据监听、事件处理等都未开始。
    • created:创建后。实例已经创建完成,即数据已经实现监听、事件已经绑定,所有需要用到的实例配置都以处理结束,但还未挂载到对应的DOM上,也就是模板尚未编译。
  2. 挂载阶段

    • befooreMount:挂载前。挂载前主要是已经完成了模板编译工作。
    • mounted:挂载后。创建vm.$el并替换了el项,完成了DOM的渲染工作(异步)。
  3. 更新阶段

    更新阶段就是数据监听到了数据变化,触发虚拟节点更新以及渲染的过程

    • beforeUpdate:更新前。数据变化并触发更新时调用。
    • updated:更新后,虚拟节点已经渲染结束,此时的DOM已经是最新的状态了,所以可以执行依赖DOM的操作,但是大多数情况下需要谨慎使用,避免循环更新数据。
  4. 销毁阶段

    • beforeDestroy:销毁前。实例销毁前调用,此时,实例任然可用。
    • destroyed:销毁后,实例销毁结束,实例关联的数据监听以及事件绑定都会被移除,所有子实例也会被销毁。

需要注意的是,keep-alive有独立的生命周期,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

参考:

Vue实例的子组件和父组件的生命周期钩子执行顺序

  • 创建和挂载阶段

    先创建父、子组件,再一起挂载。

    1. 父组件:beforeCreate、created、beforeMounted
    2. 子组件:beforeCreate、created、beforeMounrted
    3. 子组件:mounted
    4. 父组件:mounted
  • 更新阶段

    1. 父组件:beforeUpdate
    2. 子组件:beforeUpdate
    3. 子组件:updated
    4. 父组件:updated
  • 销毁过程

    1. 父组件:beforeDestroy
    2. 子组件:beforeDestroy
    3. 子组件:destroyed
    4. 父组件:destroyed

生命周期钩子created和mounted的区别

created属于创建阶段,在实例创建结束之后调用,此时数据监听、相关事件绑定已完成,但dom未渲染。

mounted属于挂载阶段,此时dom已经完成了渲染工作。

  • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
  • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。

一般在哪个生命周期请求异步数据

进行异步请求数据一般是实例已经创建结束,即created钩子中调用,虽然beforeMountmounted也可以进行调用异步请求,但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