文章目录
- JavaScript&Typescript 相关
- 判断是否是数组
- js精度丢失问题及解决办法
- 反转链表
- js数据类型
- 实现 trim 方法
- 判断一个数是否是 NaN 类型?
- Number.isNaN(value) 与 isNaN(value) 区别?
- 编写防抖函数
- 将一个数划分成逗号相隔的数?
- 如何查找当前作用域的上级作用域
- 如何明确this指向?
- 箭头函数
- 实现bind方法
- 事件委托、事件流、事件冒泡
- 数组操作
- 对闭包的理解
- 什么是函数式编程
- 字符串大小写反转
- 一堆数字字符串组成最大数是多少[50, 2, 5, 9] => 95502 (字典序+贪心)
- Map/Set、WeakMap
- 如何取消一个请求
- 如何设计封装一个组件
- js文件异步加载
- 实现call方法和apply方法
- 浏览器的渲染机制
- 关于内存释放和作用域销毁的研究
- 对象的深拷贝和浅拷贝
- js类的属性和特点
- 手写Promise.all 和 Promise.race
- settimeout实现interval(注意和普通的要无差别体验)
- 笔试题
- 拖拽控制元素宽度
JavaScript&Typescript 相关
判断是否是数组
const a = [1,1,1]
Array.isArray(a)
a instanceof Array
a.__proto__ === Array.prototype
a.constructor === Array
Object.prototype.toString.call(a) === '[object, Array]'
js精度丢失问题及解决办法
javaScript存储方式是双精度浮点数,在二进制转换时有精度丢失的问题。
对于一个整数,可以很轻易转化成十进制或者二进制。但是对于一个浮点数来说,因为小数点的存在,小数点的位置不是固定的。
在javascript语言中,
0.1 和 0.2 都会被转成二进制后无限循环(精度问题就是在这里的转换出现的)
, 在计算机的二进制表示里却是无穷的,由于存储位数限制因此存在“舍去”,精度丢失就发生了。
使用Math.js/Big.js之类的第三方库来解决相应问题。
反转链表
给出一个单链表的头节点,将该链表反转后,返回反转后的链表。
// 迭代法
function reverseList(head) {
let prev = null;
let curr = head;
while (curr !== null) {
const next = curr.next; // 保存下一节点
curr.next = prev; // 反转指针
prev = curr; // 移动prev
curr = next; // 移动curr
}
return prev; // 返回新的头节点
}
// 递归法
function reverseList2(head) {
if (head === null || head.next === null) {
return head;
}
const newHead = reverseList2(head.next); // 递归反转后续链表
head.next.next = head; // 反转指针
head.next = null; // 断开原指针
return newHead; // 返回新的头节点
}
js数据类型
- 基本数据类型:
undefined
null
number
boolean
symbol
string
bigint
- 引用数据类型:
Object
function
array
Map
Set
实现 trim 方法
熟悉下正则表达符号:
表达式 | 描述 |
---|---|
\d | 匹配一个数字字符。等价于 [0-9] 。 |
\w | 匹配任何单词字符,包括下划线。等价于 [0-9a-zA-Z_] 。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等。 |
. | 匹配几乎所有字符。 |
^ | 匹配开头,在多行匹配中匹配行开头。 |
$ | 匹配结尾,在多行匹配中匹配行结尾。 |
\b | 匹配一个单词边界,即字与空格间的位置。 |
(?=p) | 匹配p前面的位置。 |
(?!p) | 匹配不是p前面的位置。 |
// 删除左右两端空格
String.prototype.trim = function (){
return this.replace(/(^\s*)|(\s*$)/g, '');
}
// 删除左边空格
String.prototype.trim = function (){
return this.replace(/(^\s*)/g, '');
}
// 删除右边空格
String.prototype.trim = function (){
return this.replace(/(\s*$)/g, '');
}
判断一个数是否是 NaN 类型?
function isNaNFunc (value){
return value !== value;
}
// Or
function isNaNFunc (value){
return Number.isNaN(value);
}
Number.isNaN(value) 与 isNaN(value) 区别?
- isNaN
会对 value 进行转换
,获取 Number(value) 的值,如果返回 NaN,则为 true,否则为 false; - Number.isNaN
不会对 value 进行转换
,所以只有当 value 为 NaN 时才返回 true,否则返回 false;
Number.isNaN('blabla'); // false
isNaN('blabla'); // true
Number.isNaN(NaN); // true
null == null // true
null === null // true
undefined == undefined // true
undefined === undefined // true
NaN == NaN // false
NaN === NaN // false
typeof NaN // "number"
typeof null // "object"
typeof undefined // "undefined"
// NaN 代表一个范围
// null、undefined 代表一个确切的值
编写防抖函数
// 函数在最后一次调用
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
// 函数在第一次调用
function debounce_leading(func, timeout = 300){
let timer;
return (...args) => {
if (!timer) {
func.apply(this, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
timer = undefined;
}, timeout);
};
}
function saveInput(){
console.log('Saving data');
}
const processChange = debounce(() => saveInput());
将一个数划分成逗号相隔的数?
function splitNum(str){
const res = str.split('.');
const pre = res[0].split("").reverse().reduce((prev, next, index)=> {
return ((index % 3) ? next : (next + ',')) + prev
})
const last = res[1] || [];
return last.length === 0? pre: pre + '.' + last
}
如何查找当前作用域的上级作用域
看当前函数是在哪个作用域下定义的,那么它的上级作用域就是谁。 和函数在哪里执行的没有任何关系
如何明确this指向?
this代表的是当前行为执行的主体;js中的context代表的是当前行为执行的环境(区域)。
- 函数执行首先看函数名前是否有“.”,有的话,“.”前面是谁this就是谁; 没有的话,this就是window
- 自执行函数中的this永远是 window
- 给元素的某一个事件绑定方法,当事件触发的时候,执行对应的方法,方法中的this是当前的元素
- 在构造函数模式中。类中(函数体中)出现的this.xxx = xxx 中的this 指的是当前类的实例
箭头函数
对于箭头函数的this总结如下:
- 箭头函数不绑定this,箭头函数中的this相当于普通变量。
- 箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。
- 箭头函数的this无法通过bind,call,apply来直接修改(可以间接修改)。
实现bind方法
fn.call(obj, 2, 3); //6
fn.apply(obj, [2, 3]); //6
// bind的特点:
// 可以修改函数this指向。
// bind返回一个绑定了this的新函数boundFcuntion
// 支持函数柯里化,我们在返回bound函数时已传递了部分参数2,在调用时bound补全了剩余参数。
// boundFunction的this无法再被修改,使用call、apply也不行。
Function.prototype.myBind = function (target) {
const self = this;
const p = Array.prototype.slice.call(arguments, 1);
return (...arg) => {
self.apply(target ? target : self, Array.prototype.concat.apply(p, arg));
};
};
Function.prototype.myBind = function (target) {
target.fn = this;
return (...args)=> target.fn(...args)
}
事件委托、事件流、事件冒泡
- 事件委托其实本质就是利用了事件捕获和事件冒泡。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<script>
const ul = document.querySelector('ul');
// 事件绑定到其公共的祖先元素ul上
ul.addEventListener('click', function (event){
let target = event.target
// 获取到被点击节点的祖先节点,直到其父节点为ul
while (target && target.parentNode !== this){
target = target.parentNode;
}
if(target.tagName === 'LI' && target.parentNode === this){
alert(`${target.innerText}被点击了`);
}
});
let newLi = document.createElement("li");
newLi.innerText = '5';
ul.appendChild(newLi);
</script>
- 事件委托的优点:
- 只需要绑定一个事件到ul上,占用的内存更小
- 可以为动态添加的元素监听事件,不需要每添加一个元素就重新绑定一次。
- JavaScrip 中事件流模型分为 事件捕获、事件目标、事件冒泡 三个阶段。
- 第一阶段:捕获阶段,从window对象传导到目标节点
- 第二阶段:目标阶段,事件在目标节点上触发
- 第三阶段:冒泡阶段,从目标节点传回window对象
<html>
<body>
<div class="parent">
<div class="target">
</div>
</div>
</body>
</html>
事件捕获
(event capturing) 由外向内,即从 DOM 树的父到子,document -> html -> body -> parent -> target事件冒泡
(event bubbling) 由内向外,即从 DOM 树的子到父,target -> parent -> body -> html -> document目标阶段
(event target) 指的是事件冒泡的第一个阶段,也就是冒泡阶段的 target 触发事件,所以目标阶段也会被当做事件冒泡的一部分
DOM0级事件绑定 el.onclick=function(){};
DOM2级事件绑定 标准浏览器:el.addEventListener(‘click’,function(){}, false)
<!-- 如果对同一个元素同事绑定捕获事件和冒泡事件 -->
document.addEventListener('click', ()=>{console.log('document 捕获')}, true)
document.addEventListener('click', ()=>{console.log('document 冒泡')}, false)
- 从外向内,捕获前进,遇到捕获事件立即执行
- 非 target 节点,先捕获再冒泡
- target 节点,按代码书写顺序执行(无论冒泡还是捕获)
通过 event.target 获取当前事件的目标对象 target 是触发事件的最具体的元素,currenttarget是绑定事件的元素(在函数中一般等于this) stopImmediatePropagation和stopPropagation方法:
- 都可以阻止,当前事件流后续节点的事件处理程序的运行
- 区别stopImmediatePropagation还会影响当前节点的其他事件,而stopPropagation不会
数组操作
/** 会改变原数组 **/
array.shift() // 删除第一个元素
array.unshift() // 开头追加一个元素
array.pop() // 删除最后一个元素
array.push() // 向最后添加一个元素
array.fill(value,start,end) // 将数组从start到end-1的位置的值都变更为value
array.reverse() // 颠倒数组中元素的顺序
array.splice() // 向数组中添加或删除项目,返回被删除的项目,改变了原数组
array.copyWithin() // 在数组内部,将指定位置的成员复制到数组中的其他位置,返回复制后的新数组
/** 不改变原数组 **/
array.concat() // 连接两个或多个数组,返回新数组
array.join() // 将数组元素通过指定分隔符拼接成字符串,返回字符串,
array.slice() // 截取数组中的一部分,返回截取的部分数组,
array.flat() // 将二维或多维数据扁平化
array.map() // 遍历数组然后返回
array.flatMap() // 先执行map操作再执行flat操作,只能展开一层数组,返回新数组,
array.sort() // 按规则排序,返回排序后的新数组
array.reduce() // 迭代数组
对闭包的理解
闭包的形成条件:存在不同作用域的变量引用
闭包的作用:可以用于定义局部作用域的持久化变量,这些变量可以用来做缓存或者计算的中间量等。
闭包的弊端:持久化变量不会被正常释放,持续占用内存空间,很容易造成内存浪费。要注意释放
什么是函数式编程
一种编程范式,利用函数把运算过程封装起来,通过组合各种函数来计算结果
函数式编程有两个基本特点:
- 通过函数来对数据进行转换
- 通过串联多个函数来求结果
函数编程的好处 var CEOs = companies.map(c => c.CEO); 函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。
函数式编程特性
- 无副作用,反复调用相同结果
- 透明引用,只会用传递进的变量和内部变量
- 函数是一等公民
高阶函数:
- 高阶函数指的是一个函数以函数为参数,或以函数为返回值,或者既以函数为参数又以函数为返回值。
函数柯里化:
- 柯里化函数会接收一些参数,然后不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值。
字符串大小写反转
function reverseStr(str){
return str.replace(/([a-z])|([A-Z])/g, function($1){
if(/[a-z]/.test($1)){
return $1.toUpperCase()
}
return $1.toLowerCase()
})
}
一堆数字字符串组成最大数是多少[50, 2, 5, 9] => 95502 (字典序+贪心)
function getMaxNumber(arr){
return arr.sort().reduce((acc= '', cur)=> Math.max(+`${acc}${cur}`, +`${cur}${acc}`))
}
Map/Set、WeakMap
- Map的键可以是任意类型,甚至引用数据类型也可以,但要保证是同一个引用
- Set则是可以保证集合中的数据唯一性,同引用的数据也会被去重
WeakMap与Map的区别:
- Map的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
- Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的
- Map 可以被遍历, WeakMap 不能被遍历
如何取消一个请求
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort();
如何设计封装一个组件
封装的目的是为了重用,提高代码效率和代码质量
低耦合,单一职责, 可拓展性,可维护性,易读性等
js文件异步加载
渲染引擎遇到 script 标签会停下来,等到执行完脚本,继续向下渲染
- defer 是“渲染完再执行”,async 是“下载完就执行”
- defer 如果有多个脚本,会按照在页面中出现的顺序加载,多个async 脚本不能保证加载顺序
实现call方法和apply方法
function fn(num){
console.log(num)
console.log(this)
}
Function.prototype.myCall = function(target, p){
const self = this;
target.fn = self;
target.fn(p);
delete target.fn
}
Function.prototype.myApply = function(target, p){
const self = this;
target.fn = self;
target.fn(...p);
delete target.fn
}
fn.myCall({a:1},5)
浏览器的渲染机制
解析html代码 -> 构建DOM树 -> 构建css树 -> 构建布局 -> 完成渲染
关于内存释放和作用域销毁的研究
全局作用域(只有当页面关闭的时候,全局作用域才会销毁)
私有作用域(只有函数执行才会产生私有的作用域)
- 当前私有作用域中的部分内存被作用域意外的东西还在那用了,那么当前的这个作用域就不能销毁。
- 在一个私有的作用域中给DOM元素的事件绑定方法,一般情况下我们的私有作用域都不销毁。
- 不立即销毁,fn返回的函数没有被其他东西占用,但是还是需要执行一次,所以暂时不销毁,当返回的值执行完成后哦,浏览器会在空闲时把它销毁。
对象的深拷贝和浅拷贝
深拷贝
- JSON方法
- 深度优先遍历对象的所有属性
- 或者直接使用lodash等第三方库 浅拷贝
- Object.asign
- 类似于深拷贝的属性遍历,但是只遍历第一层
js类的属性和特点
- 每个函数数据类型(普通函数、类)都天生自带一个属性:prototype(原型),并且这个属性是一个对象数据类型的值。
- 并且在prototype上浏览器天生给他加了一个属性constructor(构造函数),属性值是当前函数(类)本身
- 每一个对象数据类型(普通的对象、实例、prototype…)也天生自带一个属性:proto,属性值是当前实例所属类的原型(prototype)
手写Promise.all 和 Promise.race
- 接收一个可迭代对象
- 传入的数据中可以是普通数据,也可以是Promise对象
- 可迭代对象的promise是并行执行的
- 保持输入数组的顺序和输出数组的顺序一致
- 传入数组中只要有一个reject,立即返回reject
- 所有数据resolve之后返回结果
Promise.all = (iter) => {
return new Promise((resolve, reject) => {
const targets = Array.from(iter);
const result = [];
if(targets.length === 0) {
return Promise.resolve(arr)
}
for (let i = 0; i < targets.length; i++) {
Promise.resolve(targets[i]).then(
(res) => {
result[i] = res;
if (i === targets.length - 1) {
resolve(result);
}
},
(err) => reject(err)
);
}
});
};
Promise.race = (arr) => {
return new Promise((res,rej) => {
if(arr.length === 0 ) {
return Promise.resolve(arr)
}
for(let i = 0; i < arr.length; i++){
arr[i].then(resolve => {
res(resolve) //某一promise完成后直接返回其值
}).catch(e => {
rej(e) //如果有错误则直接结束循环,并返回错误
})
}
})
}
settimeout实现interval(注意和普通的要无差别体验)
function customSetInterval(callback, delay, ...args) {
let isCleared = false;
function execute() {
if (!isCleared) {
callback(...args);
scheduleNext();
}
}
function scheduleNext() {
setTimeout(execute, delay);
}
// 立即调度第一次执行
scheduleNext();
// 返回清除函数
return function customClearInterval() {
isCleared = true;
};
}
笔试题
// 使用let替代var(块级作用域)
for(let i = 1; i <= 3; i++){ // 使用let声明i(块级作用域)
setTimeout(function(){
console.log(i); // 每个定时器捕获独立的i值
}, 1000);
}
//
for(var i = 0; i<3;i++){
((i)=>setTimeout(function(){
console.log(i)
},1000))(i)
}
// 立即执行函数(IIFE)闭包
for(var i = 1; i <= 3; i++){
(function(j){ // 立即执行函数创建闭包
setTimeout(function(){
console.log(j); // 闭包捕获参数j
}, 1000);
})(i); // 将当前i值传递给闭包
}
// 箭头函数与参数解构(ES6)
for(var i = 1; i <= 3; i++){
setTimeout((j => () => console.log(j))(i), 1000);
}
// 使用setTimeout的第三个参数
for(var i = 1; i <= 3; i++){
setTimeout(function(j){
console.log(j); // 第三个参数作为回调函数的参数
}, 1000, i); // 传递当前i值
}
console.log(i)
拖拽控制元素宽度
MouseDown = (e) => {
// 鼠标的X轴坐标
let clientX = e.clientX;
// 拖动块距离屏幕左侧的偏移量
let offsetLeft = dragBtn.offsetLeft;
// 鼠标移动
document.onmousemove = (e) => {
let curDist = offsetLeft + (e.clientX - clientX), // 拖动块的移动距离
maxDist = 700; // 拖动块的最大移动距离
// 边界值处理
if (curDist < 243) {
// 设置值(保证拖动块一直在拖动盒子的相对位置)
curDist < 32 && (curDist = 32);
this.setMinValue(curDist);
return false;
}
curDist > maxDist && (curDist = maxDist);
this.setMaxValue(curDist);
return false;
};
// 鼠标松开
document.onmouseup = (e) => {
let curDist = offsetLeft + (e.clientX - clientX); // 拖动块的移动距离
if (curDist < 243) {
this.setMinValue(32);
}
document.onmousemove = null;
document.onmouseup = null;
// 释放鼠标 ie兼容
// @ts-ignore
dragBtn?.releaseCapture && dragBtn.releaseCapture();
};
// 捕获鼠标 ie兼容
// @ts-ignore
dragBtn.setCapture && dragBtn.setCapture();
return false;
};