前端面试题视频讲解
实现类的继承
类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
代码语言:javascript复制function Parent(name) {
this.parent = name
}
Parent.prototype.say = function() {
console.log(`${this.parent}: 你打篮球的样子像kunkun`)
}
function Child(name, parent) {
// 将父类的构造函数绑定在子类上
Parent.call(this, parent)
this.child = name
}
/** 1. 这一步不用Child.prototype =Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类 2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性3. Object.create是创建了父类原型的副本,与父类原型完全隔离*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
console.log(`${this.parent}好,我是练习时长两年半的${this.child}`);
}
// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;
var parent = new Parent('father');
parent.say() // father: 你打篮球的样子像kunkun
var child = new Child('cxk', 'father');
child.say() // father好,我是练习时长两年半的cxk
复制代码
instanceof
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
const myInstanceof = (left, right) => {
// 基本数据类型都返回false
if (typeof left !== 'object' || left === null) return false;
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === null) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
复制代码
实现发布-订阅模式
代码语言:javascript复制class EventCenter{
// 1. 定义事件容器,用来装事件数组
let handlers = {}
// 2. 添加事件方法,参数:事件名 事件方法
addEventListener(type, handler) {
// 创建新数组容器
if (!this.handlers[type]) {
this.handlers[type] = []
}
// 存入事件
this.handlers[type].push(handler)
}
// 3. 触发事件,参数:事件名 事件参数
dispatchEvent(type, params) {
// 若没有注册该事件则抛出错误
if (!this.handlers[type]) {
return new Error('该事件未注册')
}
// 触发事件
this.handlers[type].forEach(handler => {
handler(...params)
})
}
// 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布
removeEventListener(type, handler) {
if (!this.handlers[type]) {
return new Error('事件无效')
}
if (!handler) {
// 移除事件
delete this.handlers[type]
} else {
const index = this.handlers[type].findIndex(el => el === handler)
if (index === -1) {
return new Error('无该绑定事件')
}
// 移除事件
this.handlers[type].splice(index, 1)
if (this.handlers[type].length === 0) {
delete this.handlers[type]
}
}
}
}
复制代码
throttle(节流)
高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。
代码语言:javascript复制const throttle = (fn, time) => {
let flag = true;
return function() {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, time);
}
}
复制代码
节流常应用于鼠标不断点击触发、监听滚动事件。
实现非负大整数相加
JavaScript对数值有范围的限制,限制如下:
代码语言:javascript复制Number.MAX_VALUE // 1.7976931348623157e 308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码
如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER
)进行加法运算,但是又想输出一般形式,那么使用 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER
数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。
实现一个算法进行大数的相加:
代码语言:javascript复制function sumBigNumber(a, b) {
let res = '';
let temp = 0;
a = a.split('');
b = b.split('');
while (a.length || b.length || temp) {
temp = ~~a.pop() ~~b.pop();
res = (temp % 10) res;
temp = temp > 9
}
return res.replace(/^0 /, '');
}
复制代码
其主要的思路如下:
- 首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化
- 初始化res,temp来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
- 将两个数组的对应的位进行相加,两个数相加的结果可能大于10,所以可能要仅为,对10进行取余操作,将结果保存在当前位
- 判断当前位是否大于9,也就是是否会进位,若是则将temp赋值为true,因为在加法运算中,true会自动隐式转化为1,以便于下一次相加
- 重复上述操作,直至计算结束
函数柯里化的实现
函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
代码语言:javascript复制function curry(fn, args) {
// 获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function() {
let subArgs = args.slice(0);
// 拼接得到现有的所有参数
for (let i = 0; i < arguments.length; i ) {
subArgs.push(arguments[i]);
}
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
复制代码
实现数组的扁平化
(1)递归实现
普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:
代码语言:javascript复制let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i ) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
复制代码
(2)reduce 函数迭代
从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:
代码语言:javascript复制let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
复制代码
(3)扩展运算符实现
这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:
代码语言:javascript复制let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码
(4)split 和 toString
可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:
代码语言:javascript复制let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',');
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码
通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组。
(5)ES6 中的 flat
我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])
其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:
代码语言:javascript复制let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码
可以看出,一个嵌套了两层的数组,通过将 flat 方法的参数设置为 Infinity,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。在编程过程中,如果数组的嵌套层数不确定,最好直接使用 Infinity,可以达到扁平化。 (6)正则和 JSON 方法 在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:
代码语言:javascript复制let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/([|])/g, '');
str = '[' str ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码
查找字符串中出现最多的字符和个数
例: abbcccddddd -> 字符最多的是d,出现了5次
代码语言:text复制let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 使其按照一定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"
// 定义正则表达式
let re = /(w)1 /g;
str.replace(re,($0,$1) => {
if(num < $0.length){
num = $0.length;
char = $1;
}
});
console.log(`字符最多的是${char},出现了${num}次`);
复制代码
使用 reduce 求和
arr = 1,2,3,4,5,6,7,8,9,10,求和
代码语言:javascript复制let arr = [1,2,3,4,5,6,7,8,9,10]
arr.reduce((prev, cur) => { return prev cur }, 0)
复制代码
arr = [1,2,3,[4,5,6],7,8,9],求和
代码语言:javascript复制let arr = [1,2,3,4,5,6,7,8,9,10]
arr.flat(Infinity).reduce((prev, cur) => { return prev cur }, 0)
复制代码
arr = {a:1, b:3}, {a:2, b:3, c:4}, {a:3},求和
代码语言:javascript复制let arr = [{a:9, b:3, c:4}, {a:1, b:3}, {a:3}]
arr.reduce((prev, cur) => {
return prev cur["a"];
}, 0)
复制代码
实现AJAX请求
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
- 创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
复制代码
手写类型判断函数
代码语言:javascript复制function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return value "";
}
// 判断数据是引用类型的情况
if (typeof value === "object") {
let valueClass = Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split("");
type.pop();
return type.join("").toLowerCase();
} else {
// 判断数据是基本数据类型的情况和函数的情况
return typeof value;
}
}
复制代码
实现一个call
call做了什么:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定this到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
// 模拟 call bar.mycall(null);
//实现一个call方法:
Function.prototype.myCall = function(context) {
//此处没有考虑context非object情况
context.fn = this;
let args = [];
for (let i = 1, len = arguments.length; i < len; i ) {
args.push(arguments[i]);
}
context.fn(...args);
let result = context.fn(...args);
delete context.fn;
return result;
};
复制代码
具体实现参考JavaScript深入之call和apply的模拟实现
实现instanceOf
代码语言:text复制// 模拟 instanceof
function instance_of(L, R) {
//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) return false;
if (O === L)
// 这里重点:当 O 严格等于 L 时,返回 true
return true;
L = L.__proto__;
}
}
复制代码
实现apply方法
apply原理与call很相似,不多赘述
代码语言:text复制// 模拟 apply
Function.prototype.myapply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i ) {
args.push("arr[" i "]");
}
result = eval("context.fn(" args ")");
}
delete context.fn;
return result;
};
复制代码
实现数组的push方法
代码语言:javascript复制let arr = [];
Array.prototype.push = function() {
for( let i = 0 ; i < arguments.length ; i ){
this[this.length] = arguments[i] ;
}
return this.length;
}
复制代码
转化为驼峰命名
代码语言:c#复制var s1 = "get-element-by-id"
// 转化为 getElementById
复制代码
代码语言:javascript复制var f = function(s) {
return s.replace(/-w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
复制代码
滚动加载
原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。
代码语言:javascript复制window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight scrollTop >= scrollHeight) {
// 检测到滚动至页面底部,进行后续操作
// ...
}
}, false);
复制代码
Promise并行限制
就是实现有并行限制的Promise调度器问题
代码语言:javascript复制class Scheduler {
constructor() {
this.queue = [];
this.maxCount = 2;
this.runCounts = 0;
}
add(promiseCreator) {
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i ) {
this.request();
}
}
request() {
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts ;
this.queue.shift()().then(() => {
this.runCounts--;
this.request();
});
}
}
const timeout = time => new Promise(resolve => {
setTimeout(resolve, time);
})
const scheduler = new Scheduler();
const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4
复制代码
将数字每千分位用逗号隔开
数字有小数版本:
代码语言:javascript复制let format = n => {
let num = n.toString() // 转成字符串
let decimals = ''
// 判断是否有小数
num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
let len = num.length
if (len <= 3) {
return num
} else {
let temp = ''
let remainder = len % 3
decimals ? temp = '.' decimals : temp
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) ',' num.slice(remainder, len).match(/d{3}/g).join(',') temp
} else { // 是3的整数倍
return num.slice(0, len).match(/d{3}/g).join(',') temp
}
}
}
format(12323.33) // '12,323.33'
复制代码
数字无小数版本:
代码语言:javascript复制let format = n => {
let num = n.toString()
let len = num.length
if (len <= 3) {
return num
} else {
let remainder = len % 3
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) ',' num.slice(remainder, len).match(/d{3}/g).join(',')
} else { // 是3的整数倍
return num.slice(0, len).match(/d{3}/g).join(',')
}
}
}
format(1232323) // '1,232,323'
复制代码
实现类数组转化为数组
类数组转换为数组的方法有这样几种:
- 通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
复制代码
- 通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
复制代码
- 通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
复制代码
- 通过 Array.from 方法来实现转换
Array.from(arrayLike);
复制代码