问题先导
- 说一下HTML5的
drag
API【html】 - 两栏布局的实现【css布局】
- 三栏布局的实现【css布局】
- 常见的位运算符【js基础】
- 为什么函数中的隐式变量arguments是类数组而不是数组?【js基础】
- BOM和DOM的区别?【js基础】
- 对React和Vue的理解,它们的异同【Vue】
- Vue的优点【Vue】
- assets和static目录的区别?【Vue】
- 实现数组去重【手写代码】
- 实现数组的
flat
方法【手写代码】 - 实现数组的
push
方法【手写代码】 - 代码输出结果(Promise相关)【代码输出】
- 翻转二叉树【算法】
- 三数之和【算法】
知识梳理
说一下HTML5的drag
API
drag
事件即拖拽事件,当元素或文本被推动时触发,同mouse
事件类似,相对于mouse
事件,drag
事件有一套更为完整的专门针对拖放的事件,拖放的过程一般有两个元素需要侦听:被拖动的元素和接收被拖动元素的目标元素。
针对两个不同的元素,有不同的事件侦听方式:
- 被拖动元素
dragstart
:元素被拖动时触发drag
:元素拖动过程中触发dragend
:拖动结束时触发
- 目标元素
dragenter
:被拖动元素进入目标元素时触发(一般也就是鼠标进入目标元素时触发)dragover
:被拖动元素在目标元素中移动时触发dragleave
:被拖动元素离开目标元素时触发drop
:拖动在目标元素内结束拖动时触发
值得注意的是,如果要让drop
触发,需要阻止dragover
的默认事件:event.preventDefault();
。
参考:
两栏布局的实现
一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应。
假设左侧宽度固定为200px
。基本html结构为:
<div class="outer">
<div class="left"></div>
<div class="right"></div>
</div>
复制代码
一般有三种方案:
-
利用浮动定位:
float
-
左侧浮动定位,设置宽度为固定宽度;右侧设置
margin-left
为左侧固定宽度值,宽度设置为auto
,就会自动撑满父元素。.outer { width: 500px; height: 500px; margin: 0px auto; border: 1px solid #ccc; } .left { float: left; width: 200px; height: 100px; background-color: #f59898; } .right { margin-left: 200px; width: auto; height: 300px; background-color: #9ed0c4; } 复制代码
-
左侧浮动定位,并设置固定宽度;右侧设置
over-flow:hidden
,这样右边就会触发BFC(Block Formatting Context)
,BFC区域不会与浮动元素重叠。.right { over-flow: hidden; width: auto; height: 300px; background-color: #9ed0c4; } 复制代码
-
-
利用绝对定位
-
将父元素设置为相对定位;左侧元素设置为绝对定位,宽度设置为固定宽度值;右侧设置
margin-left
为固定宽度值,宽度设置为calc(100% - 200px)
。需要注意的是,display
需要换成行内元素如display: inline-block
。.outer { margin: 0px auto; width: 500px; height: 500px; border: 1px solid #ccc; } .left { position: absolute; width: 200px; height: 100px; background-color: #f59898; } .right { margin-left: 200px; height: 300px; background-color: #9ed0c4; } 复制代码
-
将父元素设置为相对定位,左侧元素宽度设置为固定宽度,设置右侧为绝对定位,
left
设置为固定宽度值。.outer { position: relative; margin: 0px auto; width: 500px; height: 500px; border: 1px solid #ccc; } .left { width: 200px; height: 100px; background-color: #f59898; } .right { position: absolute; width: calc(100% - 200px); top: 0px; left: 200px; height: 300px; background-color: #9ed0c4; } 复制代码
-
-
利用
flex
布局:设置左侧元素宽度为固定值,右侧flex
设置为auto
或1
.outer { display: flex; width: 500px; height: 500px; margin: 0px auto; border: 1px solid #ccc; } .left { width: 200px; height: 100px; background-color: #f59898; } .right { flex: 1; height: 300px; background-color: #9ed0c4; } 复制代码
三栏布局的实现
三栏布局一般指的是页面中一共有三栏,左右两栏宽度固定,中间自适应的布局。
和两栏布局差不多,基本结构为:
<div class="outer">
<div class="left"></div>
<div class="right"></div>
<div class="center"></div>
</div>
复制代码
-
利用浮动
设置左右元素分别为左右浮动,中间元素再分别设置
margin-left
和margin-right
。但是,
div.center
元素必须位于div.right
之后,否则浮动会换行显示。.outer { width: 500px; height: 500px; margin: 0px auto; border: 1px solid #ccc; } .left { float: left; width: 200px; height: 100px; background-color: #f59898; } .center { margin-left: 200px; margin-right: 100px; width: auto; height: 200px; background-color: #9ed0c4; } .right { float: right; width: 100px; height: 300px; background-color: #a99ed0; } 复制代码
-
利用绝对定位
设置父元素为相对定位,左右两侧设置为绝对定位,中间再设置一下
margin
即可,和浮动定位类似,但不需要将div.center
放置到div.right
之后。.outer { position: relative; width: 500px; height: 500px; margin: 0px auto; border: 1px solid #ccc; } .left { position: absolute; left: 0px; top: 0px; width: 200px; height: 100px; background-color: #f59898; } .center { margin-left: 200px; margin-right: 100px; width: auto; height: 200px; background-color: #9ed0c4; } .right { position: absolute; right: 0px; top: 0px; width: 100px; height: 300px; background-color: #a99ed0; } 复制代码
-
利用flex定位
左右两侧设置固定宽度,中间设置
flex:1
即可。.outer { display: flex; width: 500px; height: 500px; margin: 0px auto; border: 1px solid #ccc; } .left { width: 200px; height: 100px; background-color: #f59898; } .center { flex: 1; height: 200px; background-color: #9ed0c4; } .right { width: 100px; height: 300px; background-color: #a99ed0; } 复制代码
-
圣杯布局
利用浮动和负边距来实现。将中间元素放到最前面,然后设置宽度为父级元素宽度,三列均设置为左浮动,这样左右两个元素就会被挤到下一行,然后通过设置元素的
margin
为负值,就能将下一行的这两个浮动元素移到上一行。然后再利用相对定位来调整左右元素的位置,最后再设置父元素的padding,让左右元素处于padding
之内即可。.outer { position: relative; margin: 0px auto; width: 500px; height: 500px; border: 1px solid #ccc; padding-left: 200px; padding-right: 100px; } .center { float: left; width: 100%; height: 400px; background-color: #9ed0c4; } .left { float: left; margin-left: -200px; position: relative; left: -100%; width: 200px; height: 100px; background-color: #f59898; } .right { float: left; margin-left: -100px; width: 100px; position: relative; left: 100px; height: 300px; background-color: #a99ed0; } 复制代码
但是圣杯布局有个问题,当面板的main部分比两边的子面板宽度小的时候,布局就会乱掉。但是圣杯布局有个问题,当面板的main部分比两边的子面板宽度小的时候,布局就会乱掉。想实现更完美的布局非常困难,因此双飞翼布局在主面板上选择了添加一个标签。
-
双飞翼布局
双飞翼布局相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。
<div class="outer"> <div class="center-container"> <div class="center"></div> </div> <div class="left"></div> <div class="right"></div> </div> 复制代码
.outer { position: relative; margin: 0px auto; width: 500px; height: 500px; border: 1px solid #ccc; } .center-container { float: left; width: 100%; } .center { height: 400px; margin-left: 200px; margin-right: 100px; background-color: #9ed0c4; } .left { float: left; margin-left: -100%; width: 200px; height: 100px; background-color: #f59898; } .right { float: left; margin-left: -100px; width: 100px; height: 300px; background-color: #a99ed0; } 复制代码
- 双飞翼布局给主面板添加了一个父标签用来通过margin给子面板腾出空间。
- 圣杯采用的是padding,而双飞翼采用的margin,解决了圣杯布局的问题。
- 双飞翼布局不用设置相对布局,以及对应的left和right值。
更多细节可参考:什么是圣杯布局以及双飞翼布局
如果对网格布局有了解,两列布局和三列布局均可用网格布局实现。
常见的位运算符有哪些?
二进制之间的运算就称为位运算。常见的位运算有以下几种:
运算符 | 描述 | 运算规则 |
---|---|---|
& |
与 | 两个位都为1时,结果才为1 |
` | 或 | 只要有一个为1,结果就为1 |
^ |
异或 | 两个位相同为0,相异为1 |
~ |
取反 | 0变1,1变0 |
<< |
左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 |
>> |
右移 | 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |
位运算由于直接使用二进制进行计算,节约了内存,使程序计算效率更高,不同的位运算有不同的用途。
在js中,可以通过以下运算符进行运算:
按位与(&)
都为1时结果为1,比如3&5 = 1
写成二进制就是:
0000 0011 &
0000 0101 =
0000 0001
复制代码
用途:判断数字奇偶:n & 1 === 0
和n % 2 == 0
是一样的。
按位或(|)
只要有一个为1,结果就为1。比如3 | 5 = 7
:
0000 0011 |
0000 0101 =
0000 0111
复制代码
按位异或(^)
相同为0,相异为1。所以3 ^ 5 = 6
。
0000 0011 ^
0000 0101 =
0000 0 110
复制代码
异或是很有规律的一种位运算:
- 自反性:
a ^ a = 0
,a ^ 0 = a
- 结合律:
a ^ b ^ c = a ^ (b ^ c)
- 交换律:
a ^ b = b ^ a
(这没什么好说的,别的位运算也这样)
其中最主要的就是自反性,因为交换律和结合律别的运算符一般也有这个性质。
由自反性得:a ^ b ^ b
= a ^ 0
= a
。我们可以通过这个性质交换a
和b
的值而不使用中间变量。
a = a ^ b
b = a ^ b
a = a ^ b
复制代码
取反运算符(~)
没什么好说的,0和1交换即可。
左移运算(>>)
将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。
有符号右移(>>)
该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作“符号传播”。
无符号右移(>>>)
该操作符会将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用0填充。因为符号位变成了 0,所以结果总是非负的。(译注:即便右移 0 个比特,结果也是非负的。)
原码、反码和补码
值得注意的是,二进制在计算机中均以补码进行存储,计算的时候同样使用的是补码。
数字有正负,一般以第一位比特表示正负,0表示正数,1表示负数,后面存储的位数才是真实的数值。
我们发现二进制是没有减法运算的,或者说不方便进行减法运算,那么对于减法运算我们作何处理呢?
那就是看成加法运算,比如1 - 2
等同于1 + (-2)
。百科上关于补码的概念引入了“模”的概念,比如一个时钟,能表示的时间只有0-11
小时,这种有限范围的计量系统,就称之为模。计量系统的最大计量范围也称为模,所以这里的时钟可以看做模为12的计量系统,在时钟系统中,如果要把8
时调整为6
时,有两种方法,一是往回拨2时刻,二是往前拨10
时刻,也就是做减法和做加法的区别:即8 - 2 = 8 + 10
,对于8 + 10
由于超过了模的计量范围,就只能保存余数:18 - 12 = 6
,因此这里的加法和减法是能互相转换的,而这个性质对于所有模计量系统都符合。对于减法8 - 2
转换为加法就是8 + (-2)
,我们用模加上这个负数,就得到了它的“补码”10
,用补码就能方便地进行加法运算了。
计算机也是一种模计量系统,因为计算机的存储位数是有限的,当超过这个限制,数字就会丢失,只保留能存储的数字,比如某个计算机的存储位数只有4位,那么最大的存储数字就为1111
,如果再往上加1,就变成了10000
,由于模的限制,只能保存4位,又变回了0000
。
由于二进制不方便进行减法运算,我们都要转换为加法运算,在模系统中,减法转加法就是加上它的补码即可,为了方便计算,计算机存储的都是补码,以方便直接进行运算,我们知道,补码 + 源码 = 模,对于二进制的计算机系统来说,模是不固定的,但我们找到了计算负数补码的方式:补码 = 反码 + 1,反码就是除符号位外,其余位取反即可。这里需要注意的是,补码和反码都是针对负数而言的,因为正数不需要进行减法转加法换算。
补码的计算原理很简单:原码+ 反码 = 11111...
,即全是1,再加1,就得到模,所以有原码 + 反码 + 1 = 模
,所以有补码 = 反码 + 1
。
为什么函数中的隐式变量arguments是类数组而不是数组?
为什么函数的隐式变量arguments
是类数组而不是数组?
argument
是一个对象,是专门用于收集函数参数的内置变量,argument.length
可以表示收集到的参数数量,而且可以使用arguments[n]
(n ∈ [0, arguments.length – 1])来访问数字,而且这些下标可迭代,到这里,arguments
和数组一样访问元素,但arguments
不具备数组的其他属性和方法,而且有自己的属性如callee
,因此,arguments
只是类数组而不是真正的数组,要转为数组,只需要使用Array.from(arguments)
即可。
BOM和DOM的区别?
BOM全称Browser Object Mode,即浏览器对象模型,为了方便和JavaScript进行交互,浏览器被抽象为了一个JavaScript对象,这就是BOM,可以将其理解为浏览器的抽象,也就是我们常用的Window
对象。
而DOM全称Document Object Mode,即文档模型对象,这里的文档,就是HTML文档,同样为了方便和js交互,被抽象为了一个Document对象,也是一个js对象。
而DOM对象是被包含在BOM对象中的。
对React和Vue的理解,它们的异同
官方自己进行了对比说明:对比其他框架。
相似之处:
- 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;
- 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;
- 都使用了Virtual DOM(虚拟DOM)提高重绘性能;
- 都有props的概念,允许组件间的数据传递;
- 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。
不同之处 :
1)数据流
Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM
Vue2.x开始引入”Virtual DOM”,消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
- Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
- 对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化。
3)组件化
React与Vue最大的不同是模板的编写。
- Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性。
- React推荐你所有的模板通用JavaScript的语法扩展——JSX书写。
具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 4)监听数据变化的实现原理不同
- Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
- React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
5)高阶组件
react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。
6)构建工具
两者都有自己的构建工具:
- React ==> Create React APP
- Vue ==> vue-cli
7)跨平台
- React ==> React Native
- Vue ==> Weex
Vue的优点
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb
;
简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:保留了 angular
的特点,在数据操作方面更为简单;
组件化:保留了 react
的优点,实现了 html
的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
虚拟DOM:dom
操作是非常耗费性能的,不再使用原生的 dom
操作节点,极大解放 dom
操作,但具体操作的还是 dom
不过是换了另一种方式;
运行速度更快:相比较于 react
而言,同样是操作虚拟 dom
,就性能而言, vue
存在很大的优势。
assets和static目录的区别?
相同点: assets
和 static
两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下。
不相同点:assets
中存放的静态资源文件在项目打包时,也就是运行 npm run build
时会将 assets
中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static
文件中跟着 index.html
一同上传至服务器。static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static
中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets
中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template
需要的样式文件js文件等都可以放置在 assets
中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css
等文件可以放置在 static
中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
实现数组去重
利用一些存不重复图元的数据结构进行去重,比如哈希表,也就是普通对象;或者使用集合Set
,然后在转为数组。
/**
* 数组去重
* @param {any[]} arr
*/
function uniqueArr(arr) {
if(!arr) {
return [];
}
return Array.from(new Set(arr));
}
function uniqueArr2(arr) {
if(!arr) {
return [];
}
const map = new Map();
arr.forEach(item => {
map.set(item, undefined);
});
return Array.from(map.keys());
}
复制代码
实现数组的flat方法
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
也就是数组扁平化的功能,flat
函数接收一个参数,表示提取的嵌套数组的结构深度,默认为1,
/**
* 数组的扁平化
* @param {number} depth
*/
Array.prototype.myFlat = function(depth) {
const res = [];
depth = depth || 1;
this.forEach(item => {
if(Array.isArray(item) && item.length >= depth) {
res.push(...this.myFlat.call(item, depth));
} else {
res.push(item);
}
});
return res;
};
复制代码
参考:
实现数组的push方法
push
也就是往数组中添加一个或多个参数。
Array.prototype.push = function() {
const arr = arguments.length > 0 ? Array.from(arguments) : [undefined]
arr.forEach(item => {
this[this.length] = item;
});
return arr.length;
};
复制代码
代码输出结果(Promise相关)
代码片段:
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
复制代码
考察async
和await
的用法,记住,这两个关键字只是语法糖,await
相当于Promise.then()
链式调用。
async function async1 () {
console.log('async1 start'); // 3.打印async1 start
await new Promise(resolve => { // 4.执行new Promise(),打印promise1
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start') // 1.打印srcipt start
async1().then(res => console.log(res)) // 2.执行async1
console.log('srcipt end') // 5.打印srcipt end
复制代码
打印结果为:
srcipt start
async1 start
promise1
srcipt end
复制代码
代码片段:
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise1 resolve')
}).then(res => console.log(res))
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
复制代码
执行逻辑分析:
async function async1 () {
console.log('async1 start'); // 3.打印async1 start
await new Promise(resolve => { // 4.执行new Promise()
console.log('promise1') // 5.打印promise1
resolve('promise1 resolve') // 6.更新promise状态,then的回调进入异步微任务
}).then(res => console.log(res)) // 8.打印promise1 resolve,更新promise状态,await后的代码进入异步微任务
console.log('async1 success'); // 9.打印async1 success
return 'async1 end' // 10.返回一个状态值为async1 end的成功Promise
}
console.log('srcipt start') // 1.打印srcipt start
async1().then(res => console.log(res)) // 2.执行async1() ; // 11.打印async1 end
console.log('srcipt end') // 7.打印srcipt end,宏任务执行结束,开始执行微任务
复制代码
打印结果:
srcipt start
async1 start
promise1
srcipt end
promise1 resolve
async1 success
async1 end
复制代码
翻转二叉树
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
复制代码
利用迭代翻转左右子树即可。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var invertTree = function(root) {
change(root);
return root;
};
function change(root){
if(!root){
return;
}
const left = root.left;
const right = root.right;
root.left = right;
root.right = left;
change(left);
change(right);
}
复制代码
三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
输入:nums = [0]
输出:[]
复制代码
本题是两数之和的升级版,如果使用暴力解法就是三种循环,为了优化解题方法,我们需要找到几个关键点:
- 三元组中的元素不重复:可利用排序后的数组来遍历元素,跳过重复的数字
- 复杂度降低为两数之和,然后利用双指针找到所有解
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
# 先排序
nums.sort()
ans = list()
# 枚举 a
for first in range(n):
# 需要和上一次枚举的数不相同
if first > 0 and nums[first] == nums[first - 1]:
continue
# c 对应的指针初始指向数组的最右端
third = n - 1
target = -nums[first] # 降为两数之和
# 枚举 b
for second in range(first + 1, n):
# 需要和上一次枚举的数不相同
if second > first + 1 and nums[second] == nums[second - 1]:
continue
# 需要保证 b 的指针在 c 的指针的左侧
while second < third and nums[second] + nums[third] > target:
third -= 1
# 如果指针重合,随着 b 后续的增加
# 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if second == third:
break
if nums[second] + nums[third] == target:
ans.append([nums[first], nums[second], nums[third]])
return ans
复制代码