js的内容很重要,这些是自己整理出来的内容,为了方便自己刷题,顺便分享出来,参考了很多文章和博客,希望能给大家一些帮助。
欢迎大家关注我的语雀知识库,js面试题实时更新中
https://www.yuque.com/docs/share/a95df8ad-9493-4624-846d-a921248b669d?# 《js》
1、数据类型
1.1 基本数据类型
在 JS 中共有 8 种基础的数据类型,分别为:
Undefined 、 Null 、 Boolean 、 Number 、 String 、 Object 、 Symbol 、 BigInt 。
- 值类型是直接存储在栈(stack) 中的简单数据段,占据空间⼩、⼤⼩固定,属于被频繁使⽤数据, 所以放⼊栈中存储;
- 引⽤类型存储在堆(heap) 中的对象,占据空间⼤、⼤⼩不固定。如果存储在栈中,将会影响程序 运⾏的性能;
其中 Symbol 和 BigInt 是 ES6 新增的数据类型,可能会被单独问:
• Symbol 代表独⼀⽆⼆的值,最⼤的⽤法是⽤来定义对象的唯⼀属性名。 • BigInt 可以表示任意⼤⼩的整数。
1.2 数据类型的判断
typeof:能判断所有值类型,函数。不可对 null、对象、数组进⾏精确判断,因为都返回 object 。
为什么typeof null是Object 因为在JavaScript中,不同的对象都是使⽤⼆进制存储的,如果⼆进制前三位都是0 的 话,系统会判断为是Object类型,⽽null的⼆进制全是0,⾃然也就判断为Object
instanceof:能判断对象类型,不能判断基本数据类型,其内部运⾏机制是判断在其原型链中能否找到该类型 的原型。instance.[proto…] === instance.constructor.prototype
Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
在⾯试中有⼀个经常被问的问题就是:如何判断变量是否为数组?
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
1.3 手写深拷贝
/**
* 深拷⻉
*@param {Object} obj 要拷⻉的对象
*@param {Map} map ⽤于存储循环引⽤对象的地址
**/
function deepClone(obj = {}, map = new Map()) {
if (typeof obj !== "object") { return obj; }
if (map.get(obj)) {
return map.get(obj);
}
let result = {}; // 初始化返回结果
if (obj instanceof Array ||// 加 || 的原因是为了防⽌ Array 的 prototype 被重写,Array.isArray 也是如此 Object.prototype.toString(obj) === "[object Array]") {
result = [];
}
map.set(obj, result); // 防⽌循环引⽤
for (const key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调⽤
result[key] = deepClone(obj[key], map);
}
}
// 返回结果
return result;
JSON.parse(JSON.stringify(obj))
性能最快
▪ 具有循环引⽤的对象时,报错
▪ 当值为函数、undefined、或symbol时,⽆法拷⻉
浅拷⻉
以赋值的形式拷⻉引⽤对象,仍指向同⼀个地址,修改时原对象也会受到影响
- Object.assign
- 展开运算符(…) var shallowObj2 = { …obj1 }
1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?
• 进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成⼆进制后⽆限循环 ,但是 js 采⽤ 的 IEEE 754 ⼆进制浮点运算,最⼤可以存储 53 位有效数字,于是⼤于 53 位后⾯的会全部截 掉,将导致精度丢失。
• 对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶⼩的尾数要根据阶差来右移(0舍1⼊), 尾数位移时可能会发⽣数丢失的情况,影响精度。
JavaScript使⽤Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准 通过64位来表示⼀个数字
图⽚⽂字说明 :
• 第0位:符号位,0表示正数,1表示负数(s) • 第1位到第11位:储存指数部分(e)
• 第12位到第63位:储存⼩数部分(即有效数字)f
js最⼤安全数是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1, ⽽不是Math.pow(2,52) - 1
why?尾数部分不是只有52位吗?
这是因为⼆进制表示有效数字总是1.xx…xx的形式,尾数部分f在规约形式下第⼀位默认为1(省略不写, xx…xx为尾数部分f,最⻓52位)。因此,JavaScript提供的有效数字最⻓为53个⼆进制位(64位浮点的后52 位+被省略的1位)
运算时发⽣了什么?
先按照IEEE 754转成相应的⼆进制,然后对阶运算
怎么解决精度问题?
1.将数字转成整数
这是最容易想到的⽅法,也相对简单
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
但是这种⽅法对⼤数⽀持的依然不好
2.三⽅库
这个是⽐较全⾯的做法,推荐2个库
1).Math.js
- 专⻔为 JavaScript 和 Node.js 提供的⼀个⼴泛的数学库。
- ⽀持数字,⼤数字(超出安全数的数字),复数,分数, 单位和矩阵。
- 功能强⼤,易于使⽤。
- 官⽹:http://mathjs.org/
- GitHub:https://github.com/josdejong/mathjs
2).big.js
3 转成字符串,对字符串做加法运算。
// 字符串数字相加
var addStrings = function (num1, num2) {
let i = num1.length - 1;
let j = num2.length - 1;
const res = [];
let carry = 0;
while (i >= 0 || j >= 0) {
const n1 = i >= 0 ? Number(num1[i]) : 0;
const n2 = j >= 0 ? Number(num2[j]) : 0;
const sum = n1 + n2 + carry;
res.unshift(sum % 10);
carry = Math.floor(sum / 10); i--; j--;
}
if (carry) {
res.unshift(carry);
}
return res.join("");
};
function isEqual(a, b, sum) {
const [intStr1, deciStr1] = a.toString().split(".");
const [intStr2, deciStr2] = b.toString().split(".");
const inteSum = addStrings(intStr1, intStr2); // 获取整数相加部分
const deciSum = addStrings(deciStr1, deciStr2); // 获取⼩数相加部分
return inteSum + "." + deciSum === String(sum);
}
console.log(isEqual(0.1, 0.2, 0.3)); // true
2、 原型和原型链
• 原型:每⼀个 JavaScript 对象(null 除外)在创建的时候就会与之关联另⼀个对象,这个对象就是 我们所说的原型,每⼀个对象都会从原型"继承"属性,其实就是 prototype 对象。
• 原型链:由相互关联的原型组成的链状结构就是原型链。
3、 作⽤域与作⽤域链
• 作⽤域
规定了如何查找变量,也就是确定当前执⾏代码对变量的访问权限。
换句话说,作⽤域决定了代码区块中变量和其他资源的可⻅性。(全局作⽤域、函数作⽤域、块级作⽤域)
另外,for循环还有⼀个特别之处,就是设置循环变量的那部分是⼀个⽗作⽤域,⽽循环体内部是⼀个 单独的⼦作⽤域。
• 作⽤域链
从当前作⽤域开始⼀层层往上找某个变量,如果找到全局作⽤域还没找到,就放弃寻找 。这 种层级关系就是作⽤域链。(由多个执⾏上下⽂的变量对象构成的链表就叫做作⽤域链)
什么是⾃由变量
什么叫做 ⾃由变量 。
如下代码中,console.log(x)要得到x变量,但是在当前的作⽤域中没有定义x。当前作⽤域没有定义的变量,这成为 ⾃由变量 。⾃由变量的值如何得到 —— 要到创建这个函数的那个 域”。 作⽤域中取值,这⾥强调的是 “创建” ,⽽不是“调⽤”,切记切记——其实这就是所谓的"静态作⽤域"
var x = 10 function fn() {
console.log(x) }
function show(f) {
var x = 20
(function() {
f() //10,⽽不是20
})()//立即执行函数
}
show(fn)
let块级作用域如何做到获取循环中的i
下⾯代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每⼀次循环的i其实都是⼀个新的变量,所以最后输出的是6。
你可能会问,如果每⼀轮循环的变量i都是重新声明的,那它怎么知道上⼀轮循环的值,从⽽计算出本轮循环的值?
这是因为 JavaScript 引擎内部会记住上⼀轮循环的值,初始化本轮的变量i时,就在上⼀轮循环的基础上进⾏计算
var a = []; 、
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
作⽤域与执⾏上下⽂
我们知道JavaScript属于解释型语⾔,JavaScript的执⾏分为:
解释和执⾏两个阶段,这两个阶段所做的事并不 ⼀样:
解释阶段
• 词法分析
• 语法分析
• 作⽤域规则确定
执⾏阶段
• 创建执⾏上下⽂
• 执⾏函数代码
• 垃圾回收
JavaScript解释阶段便会确定作⽤域规则,因此作⽤域在函数定义时就已经确定了,⽽不是在函数调⽤时确定
但是执⾏上下⽂是函数执⾏之前创建的。
执⾏上下⽂最明显的就是this的指向是执⾏时确定的。
⽽作⽤域 访问的变量是编写代码的结构确定的。
作⽤域和执⾏上下⽂之间最⼤的区别是:
- 执⾏上下⽂在运⾏时确定,随时可能改变;
- 作⽤域在定义时就确 定,并且不会改变。
4、 闭包
根据 MDN 中⽂的定义,闭包的定义如下:
在 JavaScript 中,每当创建⼀个函数,闭包就会在函数创建的同时被创建出来。可以在⼀个内层函数中访问到其外层函数的作⽤域。
也可以这样说:
闭包是指那些能够访问⾃由变量的函数。 ⾃由变量是指在函数中使⽤的,但既不是函数参数也不是函数的局部变量的变量。
闭包 = 函数 + 函数能够访问的⾃由变量。
在某个内部函数的执⾏上下⽂创建时,会将⽗级函数的活动对象加到内部函数的 [[scope]] 中,形成作⽤域链,所以即使⽗级函数的执⾏上下⽂销毁(即执⾏上下⽂栈弹出⽗级函数的执⾏上下⽂),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从⽽实现了闭包。
闭包应⽤
函数作为参数被传递:
function print(fn) {
const a = 200;
fn();
}
const a = 100; function fn() {
console.log(a);
}
print(fn); // 100 函数作为返回值被返回:
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
⾃由变量的查找,是在函数定义的地⽅,向上级作⽤域查找。不是在执⾏的地⽅。
5、 call、apply、bind 实现
call
call() ⽅法在使⽤⼀个指定的 this 值和若⼲个指定的参数值的前提下调⽤某个函数或⽅法。
Function.prototype.myCall = function (context) {
// 判断调⽤对象
if (typeof this !== "function") {
throw new Error("Type error");
}
// ⾸先获取参数
let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传⼊,如果没有传就设置为window
context = context || window;
// 将被调⽤的⽅法设置为 context 的属性
// this 即为我们要调⽤的⽅法
context.fn = this;
// 执⾏要被调⽤的⽅法
result = context.fn(...args);
// 删除⼿动增加的属性⽅法
delete context.fn;
// 将执⾏结果返回
return result;
};
apply
参数为数组并调⽤
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new Error("Type error");
}
let result = null;
context = context || window;
// 与call代码相⽐,我们使⽤ Symbol 来保证属性唯⼀
// 也就是保证不会重写⽤户⾃⼰原来定义在 context 中的同名属性
const fnSymbol = Symbol();
//context[fnSymbol]等同于context.fn
context[fnSymbol] = this;
// 执⾏要被调⽤的⽅法
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
bind
bind() 返回的是⼀个函数
这个地⽅可以详细阅读这篇⽂章,讲的⾮常清楚: https://github.com/sisterAn/JavaScript-Algorithms/issues/81
Function.prototype.myBind = function (context) {
// 判断调⽤对象是否为函数
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取参数
const args = [...arguments].slice(1)
const fn = this;
return function Fn() {
return fn.apply(this instanceof Fn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);};
};
6、 new 实现
1 ⾸先创⼀个新的空对象。
2 根据原型链,设置空对象的 proto 为构造函数的 prototype 。
3 构造函数的 this 指向这个对象,执⾏构造函数的代码(为这个新对象添加属性)。
4 判断函数的返回值类型,如果是引⽤类型,就返回这个引⽤类型的对象。
function myNew(context) {
const obj = new Object();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
}
7、 异步
7.1 event loop、宏任务和微任务
什么是宏任务与微任务?
我们都知道 Js 是单线程都,但是⼀些⾼耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务 的执⾏模式:同步模式(Synchronous)和异步模式(Asynchronous)。
在异步模式下,创建异步任务主要分为宏任务与微任务两种。
ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,⽽ 微任务由 JS ⾃身发起。
如何理解 script(整体代码块)是个宏任务呢
实际上如果同时存在两个 script 代码块,会⾸先在执⾏ 第⼀个 script 代码块中的同步代码,如果这个过程中创 建了微任务并进⼊了微任务队列,第⼀个 script 同步代 码执⾏完之后,会⾸先去清空微任务队列,再去开启第 ⼆个 script 代码块的执⾏。所以这⾥应该就可以理解 script(整体代码块)为什么会是宏任务。
什么是 EventLoop ?
1 判断宏任务队列是否为空
◦ 不空 --> 执⾏最早进⼊队列的任务 --> 执⾏下⼀步
◦ 空 --> 执⾏下⼀步
2 判断微任务队列是否为空
◦ 不空 --> 执⾏最早进⼊队列的任务 --> 继续检查微任务队列空不空
◦ 空 --> 执⾏下⼀步
因为⾸次执⾏宏队列中会有 script(整体代码块)任务,所以实际上就是 Js 解析完成后,在异步任务中,会先 执⾏完所有的微任务,这⾥也是很多⾯试题喜欢考察的。需要注意的是,新创建的微任务会⽴即进⼊微任务队 列排队执⾏,不需要等待下⼀次轮回。
7,2 Promise
1 Promise 是⼀个类,在执⾏这个类的时候会传⼊⼀个执⾏器,这个执⾏器会⽴即执⾏
2 Promise 会有三种状态
- Pending 等待
- Fulfilled 完成
- Rejected 失败
3 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且⼀但发⽣改变便不可⼆次修改; 4 Promise 中使⽤ resolve 和 reject 两个函数来更改状态; 5 then ⽅法内部做但事情就是状态判断 ◦ 如果状态是成功,调⽤成功回调函数 ◦ 如果状态是失败,调⽤失败回调函数
⼤家可以参考这篇⽂章讲的很详细
https://juejin.cn/post/6945319439772434469
还有promise 练习题点这⾥https://juejin.cn/post/6844904077537574919
插⼊⼀段⼿写Promose
class Promise {
// 构造函数
constructor(excutor) {
// 添加属性
this.PromiseState = 'pending'
this.PromiseResult = null
// 存放回调函数
this.callbacks = []
const self = this;
// 声明resolve
function resolve(value) {
//状态只能改变一次
if (self.PromiseState !== 'pending') return
// 函数中的this指向Window
//1.修改对象状态
self.PromiseState = 'fullfilled'
//2.改变对象结果值
self.PromiseResult = value
//判断执行的回调函数
// 异步执行时,改变状态后才执行回调
self.callbacks.forEach((callback) => {
setTimeout(() => {
callback.onResolved(value)
});
});
}
// 声明reject
function reject(value) {
//状态只能改变一次
if (self.PromiseState !== 'pending') return
// 函数中的this指向Window
//1.修改对象状态
self.PromiseState = 'rejected'
//2.改变对象结果值
self.PromiseResult = value
//判断执行的回调函数
//异步执行时,改变状态后才执行回调
self.callbacks.forEach((callback) => {
setTimeout(() => {
callback.onRejected(value)
});
});
}
try {
// 同步调用执行器函数
excutor(resolve, reject)
} catch (error) {
// 修改Promise的状态为失败
reject(error)
}
}
then(onResolved, onRejected) {
const self = this
// 判断回调参数
if (typeof onRejected !== "function") {
onRejected = (reason) => {
throw reason
}
}
if (typeof onResolved !== "function") {
onResolved = value => value
}
// 返回Promise对象
return new Promise((resolve, reject) => {
//封装try catch
function callback(type) {
try {
let result = type(self.PromiseResult)
if (result instanceof Promise) {
//如果是Promise
result.then((r) => {
resolve(r)
}, (err) => {
reject(err)
})
} else {
// 改变promise的状态 不是promise的都会为成功
resolve(result)
}
} catch (error) {
reject(error)
}
}
// 调用状态
// this指向实例对象p
if (this.PromiseState === "fullfilled") {
setTimeout(() => {
callback(onResolved)
});
}
else if (this.PromiseState === "rejected") {
setTimeout(() => {
callback(onRejected)
});
}
// 处理异步调用
else if (this.PromiseState === "pending") {
// 异步调用 保存回调函数
this.callbacks.push({
onResolved: function () {
callback(onResolved)
},
onRejected: function () {
callback(onRejected)
}
})
}
});
}
catch(onRejected) {
return this.then('undefined', onRejected)
}
// static 属于类,不属于实例对象
static resolve(value) {
return new Promise((resolve, reject) => {
if (value instanceof Promise) {
value.then((result) => {
resolve(result)
}).catch((err) => {
reject(err)
});
} else {
resolve(value)
}
});
}
static reject(error) {
return new Promise((resolve, reject) => {
reject(error)
});
}
static all(promises) {
// 返回Promise的结果数组,有一个错误则返回错误
return new Promise((resolve, reject) => {
let count = 0;
let arr = [];
for (let i = 0; i < promises.length; i++) {
promises[i].then((result) => {
//如果这个成功,存贮结果
count++;
arr.push(result)
if (count == promises.length) {
resolve(arr)
}
}, (err) => {
reject(err)
})
}
});
}
static race(promises) {
return new Promise((resolve, reject) => {
promises.forEach(item => {
item.then((result) => {
resolve(result)
}, (err) => {
reject(err)
})
});
});
}
}
7.3 async/await 和 Promise 的关系
• async/await 是消灭异步回调的终极武器。
• 但和 Promise 并不互斥,反⽽,两者相辅相成。
• 执⾏ async 函数,返回的⼀定是 Promise 对象。
• await 相当于 Promise 的 then。
• try…catch 可捕获异常,代替了 Promise 的 catch。
8、 浏览器的垃圾回收机制
两种垃圾回收策略
• 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是⾮活动对象)销毁。
• 引⽤计数:它把对象是否不再需要简化定义为对象有没有其他对象引⽤到它。如果没有引⽤指向该对象 (引⽤计数为 0),对象将被垃圾回收机制回收。
标记清除的缺点
• 内存碎⽚化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过⼤的对 象时找不到合适的块。
• 分配速度慢,因为即便是使⽤ First-fit 策略,其操作仍是⼀个 O(n) 的操作,最坏情况是每次都要遍历到 最后,同时因为碎⽚化,⼤对象的分配效率会更慢。
解决以上的缺点可以使⽤ 标记整理(Mark-Compact)算法 ,标记结束后,标记整理算法会将活着的对 象(即不需要清理的对象)向内存的⼀端移动,最后清理掉边界的内存
引⽤计数的缺点
• 需要⼀个计数器,所占内存空间⼤,因为我们也不知道被引⽤数量的上限。
• 解决不了循环引⽤导致的⽆法回收问题。
V8 的垃圾回收机制也是基于标记清除算法,不过对其做了⼀些优化。
• 针对新⽣区采⽤并⾏回收。
• 针对⽼⽣区采⽤增量标记与惰性回收。
9,继承
1.组合继承
组合继承, 有时候也叫做伪经典继承,指的是将原型链和借⽤构造函数的技术组合到⼀块,从⽽发挥两者之⻓的⼀种继承模式.
基本思路: 使⽤原型链实现对原型属性和⽅法的继承,通过借⽤构造函数来实现对实例属性的继承.
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第⼀次调⽤Father()
this.age = age;
}
Son.prototype = new Father();//继承⽗类⽅法,第⼆次调⽤Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
组合继承避免了原型链和借⽤构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常⽤的继承模式. ⽽且, instanceof 和 isPrototypeOf( )也能⽤于识别基于组合继承创建的对象.
缺点:
组合继承其实调⽤了两次⽗类构造函数, 造成了不必要的消耗,
2.原型继承
该⽅法最初由道格拉斯·克罗克福德于2006年在⼀篇题为 《Prototypal Inheritance in JavaScript》 (JavaScript中的原型式继承) 的⽂章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还 不必因此创建⾃定义类型. ⼤意如下:
在object()函数内部, 先创建⼀个临时性的构造函数, 然后将传⼊的对象作为这个构造函数的原型,最后返回了 这个临时类型的⼀个新实例.
function object(o){
function F(){}
F.prototype = o;
return new F();
}
在 ECMAScript5 中,通过新增 object.create() ⽅法规范化了上⾯的原型式继承.object.create() 接收两个参数:
• ⼀个⽤作新对象原型的对象
• (可选的)⼀个为新对象定义额外属性的对象 缺点:原型式继承中, 包含引⽤类型值的属性始终都会共享相应的值
3.寄⽣式继承
寄⽣式继承是与原型式继承紧密相关的⼀种思路, 同样是克罗克福德推⽽⼴之.
寄⽣式继承的思路与(寄⽣)构造函数和⼯⼚模式类似, 即创建⼀个仅⽤于封装继承过程的函数,该函数在内部 以某种⽅式来增强对象,最后再像真的是它做了所有⼯作⼀样返回对象.
function createAnother(original){
var clone = object(original);
//通过调⽤object函数创建⼀个新对象
clone.sayHi = function(){
//以某种⽅式来增强这个对象
alert("hi");
};
return clone;//返回这个对象
}
缺点:使⽤寄⽣式继承来为对象添加函数, 会由于不能做到函数复⽤⽽降低效率;这⼀点与构造函数模式类似.
4.寄⽣组合式继承
基本思路: 不必为了指定⼦类型的原型⽽调⽤超类型的构造函数
function SuperType() {
this.property = true;
this.say2 = function() {
console.log("222");
}
}
SuperType.prototype.getSuperValue = function() {
console.log("yes");
};
function SubType(name, sex) {
SuperType.call(this) //组合父构造函数this指向子
this.name = name;
this.sex = sex;
this.say1 = function() {
console.log(hi111);
}
}
function inherit(son, father) {
//构造一个对象obj,原型对象为father
var obj = Object.create(father.prototype); //原型式
//将son 赋值obj的构造函器 obj的原型对象指向father
obj.constructor = son; //增强对象
//将obj这个对象给son的原型对象
son.prototype = obj;
}
inherit(SubType, SuperType);
let sub1 = new SubType("lili", 12)
let sub2 = new SuperType();
console.log(sub1.__proto__);//super
console.log(sub1.say2);
console.log(sub2);
5,ES6的extend
//class 相当于es5中构造函数
//class中定义方法时,前后不能加function,全部定义在class的protopyte属性中
//class中定义的所有方法是不可枚举的
//class中只能定义方法,不能定义对象,变量等
//class和方法内默认都是严格模式
//es5中constructor为隐式属性
class People{
constructor(name='wang',age='27'){
this.name = name;
this.age = age;
console.log(`${this.name} ${this.age} eat food`)
}
eat(){
console.log(`${this.name} ${this.age} eat food`)
}
}
//继承父类
class Woman extends People{
constructor(name = 'ren',age = '27'){
//继承父类属性
super(name, age);
console.log(`${this.name} ${this.age} `)
}
eat(){
//继承父类方法
super.eat()
}
}
let wonmanObj=new Woman('xiaoxiami');
wonmanObj.eat();
10,模块化
模块化开发在现代开发中已是必不可少的⼀部分,它⼤⼤提⾼了项⽬的可维护、可拓展和可协作性。通常, 我们 在浏览器中使⽤ ES6 的模块化⽀持,在 Node 中使⽤ commonjs 的模块化⽀持。
分类:
◦ es6: import / export ◦ commonjs: requiremodule.exports / exports
◦ amd: require / defined
require与import的区别
◦ require⽀持 动态导⼊,import不⽀持,正在提案 (babel 下可⽀持) ◦ require是 同步 导⼊,import属于 异步 导⼊
◦ require是 值拷⻉,导出值变化不会影响导⼊值;import指向 内存地址,导⼊值会随导出值⽽变化
11·防抖与节流
防抖与节流函数是⼀种最常⽤的 ⾼频触发优化⽅式,能对性能有较⼤的帮助。
防抖 (debounce)
JavaScript 专题之跟着 underscore 学防抖 https://github.com/mqyqingfeng/Blog/issues/22
将多次⾼频操作优化为只在最后⼀次执⾏,通常使⽤的场景是:⽤户输⼊,只需再输⼊完成后做⼀次输⼊校验即可。
function debounce(fn, delay) {
let time = null; //只执行一次=null
return function() { //返回函数
let context = this;
if (time) { //清除掉time 后重新赋值time
clearTimeout(time);
}
time = setTimeout(function() {
fn.apply(context,...arguments[0])
// fn(arg);
}, delay)
}
}
function fullPage(e) {
console.log(e);
console.log('滚动了');
}
document.onwheel = debounce(fullPage, 1000)
节流(throttle)
JavaScript 专题之跟着 underscore 学节流 https://github.com/mqyqingfeng/Blog/issues/26
每隔⼀段时间后执⾏⼀次,也就是降低频率,将⾼频操作优化成低频操作,通常使⽤场景: 滚动 条事件 或者 resize 事件,通常每隔 100~500 ms执⾏⼀次即可。
// 使⽤时间戳
function throttle(func, wait) {
let preTime = 0;
return function () {
let nowTime = +new Date();
let context = this;
let args = arguments;
if (nowTime - preTime > wait) {
func.apply(context, args);
preTime = nowTime;
}
};
}
// 定时器实现
function throttle(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
12 ES6/ES7
由于 Babel 的强⼤和普及,现在 ES6/ES7 基本上已经是现代化开发的必备了。通过新的语法糖,能让代码 整体更为简洁和易读。
1 声明(let const)
◦ let / const: 块级作⽤域、不存在变量提升、暂时性死区、不允许重复声明
◦ const: 声明常量,⽆法修改
var声明变量可以重复声明,⽽let不可以重复声明 var是不受限于块级的,⽽let是受限于块级 var会与window相映射(会挂⼀个属性),⽽let不与window相映射 var可以在声明的上⾯访问变量,⽽let有暂存死区,在声明的上⾯访问变量会报错 const声明之后必须赋值,否则会报错
const定义不可变的量,改变了就会报错 const和let⼀样不会与window相映射、⽀持块级作⽤域、在声明的上⾯访问变量会报错
2 箭头函数
(1)⽤了箭头函数,this就不是指向window,⽽是⽗级(指向是可变的)所以箭头函数中的this的指向在它在 定义时⼀家确定了,之后不会改变。
(2)不能够使⽤arguments对象 箭头函数没有prototype
(3)不能⽤作构造函数,这就是说不能够使⽤new命令,否则会抛出⼀个错误
(4)不可以使⽤yield命令,因此箭头函数不能⽤作 Generator 函数
(5) call()、apply()、bind()等⽅法不能改变箭头函数中的this指向
3 模版字符串
基本的字符串格式化。
将表达式嵌⼊字符串中进⾏拼接。 ⽤${}来界定在ES5时我们通过反斜杠()来做多⾏字符串或者字符串⼀⾏⾏拼接。 ES6反引号(``)就能解决类模板字符串的功能
4 Set / Map: 新的数据结构
应⽤场景Set⽤于数据重组,Map⽤于数据储存,都可以for…of 遍历
Set:
(1)成员不能重复 、Set的值是唯⼀的可以做数组去重 (2)只有键值没有键名
Map:
(1)本质上是健值对的集合
(2)可以遍历,可以跟各种数据格式转换
5 异步解决⽅案
◦ Promise的状态 :pending、fulfilled、reject 两个过程:padding -> fulfilled、padding -> rejected当pending为rejectd 时,会进⼊catch
◦ generator:
▪ yield: 暂停代码
▪ next(): 继续执⾏代码
6 理解 async/await以及对Generator的优势
async、await 是⽤来解决异步的,async函数是Generator函数的语法糖
使⽤关键字async来表示,在函数内部使⽤ await 来表示异步
async函数返回⼀个 Promise 对象,可以使⽤then⽅法添加回调函数
当函数执⾏的时候,⼀旦遇到await就会先返回,等到异步操作完成,再接着执⾏函数体内后⾯的语句
async较Generator的优势
(1)内置执⾏器。
Generator 函数的执⾏必须依靠执⾏器,⽽ Aysnc 函数⾃带执⾏器,调⽤⽅式跟普通函数的调⽤ ⼀样
(2)更好的语义。
async 和 await 相较于 * 和 yield 更加语义化
(3)更⼴的适⽤性。
yield命令后⾯只能是 Thunk 函数或 Promise对象,async函数的await后⾯可以是Promise也可以 是原始类型的值
(4)返回值是 Promise。
async 函数返回的是 Promise 对象,⽐Generator函数返回的Iterator对象⽅便,可以直接使 ⽤ then() ⽅法进⾏调⽤
7 forEach、for in、for of三者区别
- forEach更多的⽤来遍历数组
- for in ⼀般常⽤来遍历对象或json ,循环出的是key
- for of数组对象都可以遍历,遍历对象需要通过Object.keys() ,循环出的是value
8 AST
抽象语法树 (Abstract Syntax Tree),是将代码逐字⺟解析成 树状对象 的形式。这是语⾔之间的转换、代码语法 检查,代码⻛格检查,代码格式化,代码⾼亮,代码错误提示,代码⾃动补全等等的基础。
function square(n){ return n * n }
通过解析转化成的AST如下图:
9,babel编译原理
• babylon 将 ES6/ES7 代码解析成 AST
• babel-traverse 对 AST 进⾏遍历转译,得到新的 AST • 新 AST 通过 babel-generator 转换成 ES5
10. 函数柯⾥化
在⼀个函数中,⾸先填充⼏个参数,然后再返回⼀个新的函数的技术,称为函数的柯⾥化。通常可⽤于在不侵 ⼊函数的前提下,为函数 预置通⽤参数,供多次重复调⽤。
const add = function add(x) {
return function (y) {
return x + y
}
}
const add1 = add(1)
add1(2) === 3 add1(20) === 21
11.数组(array)
• map: 遍历数组,返回回调返回值组成的新数组
• forEach: ⽆法break,可以⽤try/catch中throw new Error来停⽌ • filter: 过滤
• some: 有⼀项返回true,则整体为true
• every: 有⼀项返回false,则整体为false
• join: 通过指定连接符⽣成字符串
• push / pop: 末尾推⼊和弹出,改变原数组, push 返回数组⻓度, pop 返回原数组最后⼀项; • unshift / shift: 头部推⼊和弹出,改变原数组,unshift 返回数组⻓度,shift 返回原数组第⼀项 ; • sort(fn) / reverse: 排序与反转,改变原数组
• concat: 连接数组,不影响原数组, 浅拷⻉
• slice(start, end): 返回截断后的新数组,不改变原数组
• splice(start, number, value…): 返回删除元素组成的数组,value 为插⼊项,改变原数组
• indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
• reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执⾏,prev 为上次化简函数的return值,cur 为当前值
◦ 当传⼊ defaultPrev 时,从第⼀项开始;
◦ 当未传⼊时,则为第⼆项
• 数组乱序:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function () {
return Math.random() - 0.5;
});
• 数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() {
return this.toString().split(',').map(item => +item ) }
web 存储
cookie,session localStorage 和 sessionStorage。
1、cookie
• 本身⽤于浏览器和 server 通讯。
是保存在客户端的,⼀般由后端设置值,可以设置过期时间 储存⼤⼩只有4K。⼀般⽤来保存⽤户的
• 被“借⽤”到本地存储来的。 • 可⽤ document.cookie = ‘…’ 来修改。 其缺点:
• 存储⼤⼩限制为 4KB。
• http 请求时需要发送到服务端,增加请求数量。 • 只能⽤ document.cookie = ‘…’ 来修改,太过简陋 • 在http下cookie是明⽂传输的, 较不安全
cookie属性
http-only:不能被客户端更改访问,防⽌XSS攻击(保证cookie安全性的操作) Secure:只允许在https下传输
Max-age: cookie⽣成后失效的秒数
expire: cookie的最⻓有效时间,若不设置则cookie⽣命期与会话期相同
2.session
session是保存在服务端的 session的运⾏依赖sessionId,⽽sessionId⼜保存在cookie中,所以如果禁⽤的 cookie,session也是不能⽤的,不过硬要⽤也可以,可以把sessionId保存在URL 中
session⼀般⽤来跟踪⽤户的状态
session 的安全性更⾼,保存在服务端,不过⼀般为使服务端性能更加,会考虑 部分信息保存在cookie中
3.localStorage 和 sessionStorage
• HTML5 专⻔为存储来设计的,最⼤可存 5M。 • API 简单易⽤, setItem getItem。 • 不会随着 http 请求被发送到服务端。 它们的区别:
• localStorage 数据会永久存储,除⾮代码删除或⼿动删除。 • sessionStorage 数据只存在于当前会话,浏览器关闭则清空。 • ⼀般⽤ localStorage 会多⼀些。
localstorage存满了怎么办?
划分域名,各域名下的存储空间由各业务组统⼀规划使⽤ 跨⻚⾯传数据:考虑单⻚应 ⽤、采⽤url传输数据 最后兜底⽅案:情调别⼈的存储
怎么使⽤cookie保存⽤户信息
document.cookie(“名字 = 数据;expire=时间”)
怎么删除cookie
⽬前没有提供删除的操作,但是可以把它的Max-age设置为0,也就是⽴⻢失 效,也就是删除了
设计模式
参考文章 https://juejin.cn/post/6844904099704471559
1.MVVM(Model-View-ViewModel)
如上图所示:MVVM模式是在MVP模式的基础上进⾏了改良,将Presenter改良成ViewModel(抽象视图):
• ViewModel:内部集成了Binder(Data-binding Engine,数据绑定引擎),在MVP中派发器View或Model的更新都 需要通过Presenter⼿动设置,⽽Binder则会实现View和Model的双向绑定,从⽽实现View或Model的⾃动更新。
• View:可组件化,例如⽬前各种流⾏的UI组件框架,View的变化会通过Binder⾃动更新相应的Model。
• Model:Model的变化会被Binder监听(仍然是通过观察者模式),⼀旦监听到变化,Binder就会⾃动实现视图的更新。
好处
例如:
• 提升了可维护性,解决了MVP⼤量的⼿动同步的问题,提供双向绑定机制。
• 简化了测试,同步逻辑是交由Binder处理,View跟着Model同时变更,所以只需要保证Model的正确性,View就正 确。
额外的问题
• 产⽣性能问题,对于简单的应⽤会造成额外的性能消耗。
• 对于复杂的应⽤,视图状态较多,视图状态的维护成本增加,ViewModel构建和维护成本⾼。
对前端开发⽽⾔MVVM是⾮常好的⼀种设计模式。在浏览器中,路由层可以将控制权交由适当的ViewModel,后者⼜可以更新并响应持续的View,并且通过⼀些⼩修改MVVM模式可以很好的运⾏在服务器端,其中的原因就在于Model与 View已经完全没有了依赖关系(通过View与Model的去耦合,可以允许短暂View与持续View的并存),这允许View经 由给定的ViewModel进⾏渲染。
⽬前流⾏的框架Vue、React以及Angular都是MVVM设计模式的⼀种实现,并且都可以实现服务端渲染。需要注意⽬前 的Web前端开发和传统Model2需要模板引擎渲染的⽅式不同,通过Node启动服务进⾏⻚⾯渲染,并且通过代理的⽅式 转发请求后端数据,完全可以从后端的苦海中脱离,这样⼀来也可以⼤⼤的解放Web前端的⽣产⼒。
2. 观察者模式和发布/订阅模式
观察者模式
观察者模式是使⽤⼀个subject⽬标对象维持⼀系列依赖于它的observer观察者对象,将有关状态的任何变更 ⾃动通知给这⼀系列观察者对象。当subject⽬标对象需要告诉观察者发⽣了什么事情时,它会向观察者对象 们⼴播⼀个通知。
如图所示:⼀个或多个观察者对⽬标对象的 状态感兴趣时,可以将⾃⼰依附在⽬标对象 上以便注册感兴趣的⽬标对象的状态变化, ⽬标对象的状态发⽣改变就会发送⼀个通知 消息,调⽤每个观察者的更新⽅法。如果观 察者对⽬标对象的状态不感兴趣,也可以将 ⾃⼰从中分离。
发布/订阅模式
发布/订阅模式使⽤⼀个事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应⽤程序的特定事 件,这些事件可以传递⾃定义参数,⾃定义参数包含订阅者需要的信息,采⽤事件通道可以避免发布者和订阅者之 间产⽣依赖关系。
两者的区别
- 观察者模式:允许观察者实例对象(订阅者)执⾏适当的事 件处理程序来注册和接收⽬标实例对象(发布者)发出的通 知(即在观察者实例对象上注册update⽅法),使订阅者 和发布者之间产⽣了依赖关系,且没有事件通道。不存 在封装约束的单⼀对象,⽬标对象和观察者对象必须合 作才能维持约束。 观察者对象向订阅它们的对象发布其 感兴趣的事件。通信只能是单向的。
- 发布/订阅模式:单⼀⽬标通常有很多观察者,有时⼀个 ⽬标的观察者是另⼀个观察者的⽬标。通信可以实现双 向。该模式存在不稳定性,发布者⽆法感知订阅者的状态。
算法
1.冒泡排序
参考文章 1.1 冒泡排序 | 菜鸟教程 (runoob.com)
(冒泡排序的写法有很多种,所以选择一种自己喜欢的就好)
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len-1; i++) {
for (let j = 0; j < len-i-1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
return arr;
}
2、快速排序
快速排序(Quicksort)的Javascript实现 - 阮一峰的网络日志 (ruanyifeng.com)
很清晰的视频: https://www.bilibili.com/video/BV1at411T75o?from=search
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
3 是否回⽂
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。
var isPalindrome = function (x) {
if (x < 0) {
return false
}
var x2 = parseInt(x.toString().split('').reverse().join(''))
return x2 === x ? true : false
}
isPalindrome(121)
console.log(isPalindrome(121))
4 斐波那契数列
斐波那契数列又被称为黄金分割数列,指的是这样的一个数列:1,1,2,3,5,8,13,21,34…
// num1前⼀项 // num2当前项
function fb(n, num1 = 1, num2 = 1) {
if(n == 0)
return 0
if (n <= 2) {
return num2
} else {
return fb(n - 1, num2, num1 + num2)
}
}
5、去重
• 利⽤ ES6 set 关键字:
function unique(arr) {
return [...new Set(arr)];
}
• 利⽤ ES5 filter ⽅法:
function unique(arr) {
return arr.filter((item, index, array) => {
return array.indexOf(item) === index;
});
}
6、instanceof
这个⼿写⼀定要懂原型及原型链。
function myInstanceof(target, origin) {
if (typeof target !== "object" || target === null)
return false;
if (typeof origin !== "function")
throw new TypeError("origin must be function");
let proto = Object.getPrototypeOf(target);
// 相当于 proto = target.__proto__;
while (proto) {
if (proto === origin.prototype)
return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
7 数组扁平化
重点,不要觉得⽤不到就不管,这道题就是考察你对 js 语法的熟练程度以及⼿写代码的基本能⼒。
function flat(arr, depth = 1) {
if (depth > 0) {
// 以下代码还可以简化,不过为了可读性
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
}, []);
}
return arr.slice();
}
8 ⼿写 reduce
先不考虑第⼆个参数初始值:
Array.prototype.reduce = function (cb) {
const arr = this; //this就是调⽤reduce⽅法的数组
let total = arr[0]; // 默认为数组的第⼀项
for (let i = 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
考虑上初始值:
Array.prototype.reduce = function (cb, initialValue) {
const arr = this;
let total = initialValue || arr[0]; // 有初始值的话从0遍历,否则从1遍历
for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
面试题博客推荐
这两个网址是我常用的刷题的网站,都是些大佬做的
前端常见面试题总结 | 大厂面试题每日一题 (shanyue.tech)
资源
因为我习惯用ipad反复背题,所以整理了pdf版本、markDown版本和goodnotes版本(会不定时更新)需要的小伙伴可以评论区或者私信踢我