# js
# 数据类型
# 类型分类
分两大类,共八种类型。
基本类型: Number、String、Null、Undefined、Boolean、Symbol、BigInt
引用类型: Object
# 基本类型、引用类型区别
- 基本类型
- 存放在栈内存
- 数据的比较是值的比较
- 引用类型
- 栈内存存放指向堆内存中的地址。真实数据存储在堆内存中
- 数据的比较是引用地址的比较
# undefined与null的区别
是否相等
null == undefined true
null === undefined false
undefined值是派生自null值
与JavaScript的历史有关。1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示"无"的值。 根据C语言的传统,null被设计成可以自动转为0。
Number(null)0
Number(undefined)NaN
但是,JavaScript的设计者Brendan Eich,觉得这样做还不够,有两个原因。 首先,null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,Brendan Eich觉得表示"无"的值最好不是对象。 其次,JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich觉得,如果null自动转为0,很不容易发现错误。 因此,Brendan Eich又设计了一个undefined。
用法
null表示"没有对象",即该处不应该有值。典型用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。
# symbol有什么用
做唯一不重复的键值
不用new,不是构造函数。像函数一样调用
做私有属性。不会被
for in
、Object.keys
、JSON.stringify
之类的获取到。只能通过
Object.getOwnPropertySymbols
获取
# BigInt
JS中,按照IEEE 754-2008 (opens new window)标准的定义,所有数字都以双精度64位浮点 (opens new window)格式表示。
在此标准下,无法精确表示的非常大的整数将自动四舍五入。确切地说,JS 中的Number
类型只能安全地表示-9007199254740991 (-(2^53-1))
和9007199254740991(2^53-1)
之间的整数,任何超出此范围的整数值都可能失去精度。
console.log(9999999999999999); // → 10000000000000000
该整数大于JS Number 类型所能表示的最大整数,因此,它被四舍五入的。意外四舍五入会损害程序的可靠性和安全性。这是另一个例子:
// 注意最后一位的数字
9007199254740992 === 9007199254740993; // → true
JS 提供Number.MAX_SAFE_INTEGER
常量来表示 最大安全整数,Number.MIN_SAFE_INTEGER
常量表示最小安全整数:
const minInt = Number.MIN_SAFE_INTEGER;
console.log(minInt); // → -9007199254740991
console.log(minInt - 5); // → -9007199254740996
// notice how this outputs the same value as above
console.log(minInt - 4); // → -9007199254740996
BigInt
是一种新的数据类型,用于当整数值大于Number
数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。
重要的是要记住,不能使用Number
和BigInt
操作数的混合执行算术运算,需要通过显式转换其中的一种类型。 此外,出于兼容性原因,不允许在BigInt
上使用一元加号(+
)运算符。
只支持整数,不支持浮点数
计算后转成字符串即可得出计算结果
详细:
https://segmentfault.com/a/1190000019912017
# 没有BigInt之前怎么办
使用JSBI (opens new window)库,它是BigInt
提案的纯JS实现。
这个库提供了一个与原生BigInt
行为完全相同的API。下面是如何使用JSBI:
import JSBI from './jsbi.mjs';
const b1 = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
const b2 = JSBI.BigInt('10');
const result = JSBI.add(b1, b2);
console.log(String(result)); // → '9007199254741001'
使用JSBI
的一个优点是,一旦浏览器支持,就不需要重写代码。 相反,可以使用babel
插件自动将JSBI代码编译为原生 BigInt
代码。
# 手写
let a = "9007199254740991";
let b = "1234567899999999999";
function add(a ,b){
//取两个数字的最大长度
let maxLength = Math.max(a.length, b.length);
//用0去补齐长度
a = a.padStart(maxLength , 0);//"0009007199254740991"
b = b.padStart(maxLength , 0);//"1234567899999999999"
//定义加法过程中需要用到的变量
let t = 0;
let f = 0; //"进位"
let sum = "";
for(let i=maxLength-1 ; i>=0 ; i--){
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t/10);
sum = t%10 + sum;
}
if(f == 1){
sum = "1" + sum;
}
return sum;
}
# 创建对象方法对比
# Obejct.create()
Object.create(null)
可以创建不含Object原型链的对象
Object.create({x:1})
如果传了对象,则创建的对象是个空对象,传入的对象在其原型链上
Object.create2 = function(proto, propertyObject = undefined) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
function F() {}
F.prototype = proto
const obj = new F()
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
if (proto === null) {
// 创建一个没有原型对象的对象,Object.create(null)
obj.__proto__ = null
}
return obj
}
# new Object()
传null也是继承了Object的原型链
如果传了对象,
var c = {a:1}
var a = new Object(c)
a
{a: 1}
c
{a: 1}
a === c
true
只可以传一个参数
# {}
{}是javascript对象字面量创建的形式,其本质和new Object()并无区别,默认都是继承了Object对象上的prototype
# 0.1+0.2!=0.3
基于浮点计算的语言都存在这问题
0.1+0.2无法精确表达是因为二进制的原因,无法精确表示不能被整除的,存在精度舍入
// 违反常识,但JS中的确如此
console.assert(0.1 + 0.2 !== 0.3);
// 断言失败,报错
console.assert(0.1 + 0.2 === 0.30000000000000008);// 0.30000000000000001
// 断言成功
console.assert(0.1 + 0.2 === 0.30000000000000007);// 0.30000000000000004
console.assert(0.1 + 0.2 === 0.30000000000000006);// 0.30000000000000004
console.assert(0.1 + 0.2 === 0.30000000000000005);// 0.30000000000000004
console.assert(0.1 + 0.2 === 0.30000000000000004);// 0.30000000000000004
console.assert(0.1 + 0.2 === 0.30000000000000003);// 0.30000000000000004
console.assert(0.1 + 0.2 === 0.30000000000000002);// 0.30000000000000004
// 断言失败,报错
console.assert(0.1 + 0.2 === 0.30000000000000001);// 0.3
// JS中数字字面量会四舍五入到最近的双精度浮点。(Ties To Even,这是默认的舍入方式,二进制舍零进一)
因此字面量`0.30000000000000007`会变成`0.30000000000000004`。
而`0.1+0.2===0.30000000000000004`
所以不推荐直接用浮点数进行计算,需转成整数后计算
# 函数命名的区别
匿名函数
var a = function () {}
命名函数
function a() {}
区别
匿名函数会就是变量,会有变量提升,在代码前调用会报函数不存在
命名函数也存在提升,但是会提到最前,所以在哪儿都可以调用
# 举例一下 Map 和 object 的区别
如果需要一个字典的需求,都是 key: value 的形式,那应该怎么选择这两个呢
https://blog.csdn.net/ckwang6/article/details/89215396
# Map 和 WeakMap 有什么区别
https://www.jianshu.com/p/8dee04b59762
WeakMap
只接受对象作为键名(null除外),不接受其他类型的值作为键名; 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的; 不能遍历,方法有get、set、has、delete;
之所以有weak出现,是因为对象做键值的时候,无法被垃圾回收,容易造成内存泄漏
# 类型判断
# typeof
可以判断基本类型,除了null
Type | Result |
---|---|
Undefined (opens new window) | "undefined" |
Null (opens new window) | "object" (see below (opens new window)) |
Boolean (opens new window) | "boolean" |
Number (opens new window) | "number" |
BigInt (opens new window) (new in ECMAScript 2020) | "bigint" |
String (opens new window) | "string" |
Symbol (opens new window) (new in ECMAScript 2015) | "symbol" |
Function (opens new window) object (implements [[Call]] in ECMA-262 terms) | "function" |
Any other object | "object" |
# typeof null是object
JS类型值是存在32 BIT 单元里,32位有1-3位表示TYPE TAG,其它位表示真实值 而表示object的标记位正好是低三位都是0 000: object. The data is a reference to an object.
而js 里的Null 是机器码NULL空指针, (0x00 is most platforms).所以空指针引用 加上 对象标记还是0,最终体现的类型还是object.
在ECMA6中, 曾经有提案为历史平反, 将type null的值纠正为null, 但最后提案被拒了. 理由是历史遗留代码太多, 不想得罪人, 不如继续将错就错当和事老, 参考 [harmony:typeof_null ES Wiki] (opens new window)
# typeof function
function本质上也是一个对象,但是function对象与普通对象相比,其内部有一个[[Call]]方法,用来表示这个对象是可调用的,typeof操作符在判断Object时,如果内部实现了[[Call]]方法,就返回function。
# instanceof
instanceof
运算符用来检测 constructor.prototype
是否存在于参数 object
的原型链上。
原型链是可以被修改的,所以这是不安全的判断方法。
# 一些容易出错的点
var simpleStr = "This is a simple string";
var myString = new String();
var newStr = new String("String created with constructor");
var myDate = new Date();
var myObj = {};
var myNonObj = Object.create(null);
simpleStr instanceof String; // 返回 false, simpleStr并不是对象
myString instanceof String; // 返回 true
newStr instanceof String; // 返回 true
myString instanceof Object; // 返回 true
myObj instanceof Object; // 返回 true, 尽管原型没有定义
({}) instanceof Object; // 返回 true, 同上
myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法
myString instanceof Date; // 返回 false
myDate instanceof Date; // 返回 true
myDate instanceof Object; // 返回 true
myDate instanceof String; // 返回 false
# 手写instanceof
核心: 原型链的向上查找。
function myInstanceof (left, right) {
// 基本数据类型直接返回false
if (typeof left !== 'object' || left === null) return false
// getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left)
while (true) {
// 查找到尽头,还没找到
if (proto == null) return false
// 找到相同的原型对象
if (proto == right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
# Object.prototype.toString
toString()
方法返回一个表示该对象的字符串
如果此方法在自定义对象中未被覆盖,toString()
返回 "[object type]",其中 type
是对象的类型
var toString = Object.prototype.toString;
toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]
//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
封装
var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
})
function type(obj) {
// 一箭双雕
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}
# Array.isArray
// 下面的函数调用都返回 true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
Array.isArray(new Array('a', 'b', 'c', 'd'))
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype);
// 为什么是数组还是像对象那样调用,且用Array.prototype[0]这样是取不到的
Polyfill
假如不存在 Array.isArray(),则在其他代码之前运行下面的代码将创建该方法。
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
# 手写
function typeOf(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
# 深浅拷贝
浅拷贝:只考虑对象类型。
function shallowCopy(obj) {
if (typeof obj !== 'object') return
let newObj = obj instanceof Array ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。
function deepClone(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return newObj;
}
复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。
const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;
function deepClone(target, map = new WeakMap()) {
if (map.get(target)) {
return target;
}
// 获取当前值的构造函数:获取它的类型
let constructor = target.constructor;
// 检测当前对象target是否与正则、日期格式对象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)) {
// 创建一个新的特殊对象(正则类/日期类)的实例
return new constructor(target);
}
if (isObject(target)) {
map.set(target, true); // 为循环引用的对象做标记
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
# 详细
# 类型转换
# 题目
用对象做对象的键,键会变成[object object]
var a = { name: "Sam" };
var b = { name: "Tom" };
var o = {};
o[a] = 1;
o[b] = 2;
console.log(o[a]);
// 2
# this
# 什么是this
- this是JavaScript的关键字之一。它是 对象 自动生成的一个内部对象,只能在 对象 内部使用。随着函数使用场合的不同,this的值会发生变化。
- this指向什么,完全取决于 什么地方以什么方式调用,而不是 创建时。(比较多人误解的地方)(它非常语义化,this在英文中的含义就是 这,这个 ,但这其实起到了一定的误导作用,因为this并不是一成不变的,并不一定一直指向当前 这个)
# this
绑定规则
- 谁调用的就是谁,没给就是window
- call、apply、bind主动改的
- new
# this绑定优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
# 总结
如果函数被
new
修饰this绑定的是新创建的对象,例:var bar = new foo(); 函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
如果函数是使用
call,apply,bind
来调用的this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
如果函数是在某个 上下文对象 下被调用
this绑定的是那个上下文对象,例 : var obj = { foo : foo }; obj.foo(); foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
如果都不是,即使用默认绑定
例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined). 这样的绑定方式叫 默认绑定 .
# 参考文档
https://segmentfault.com/a/1190000011194676
# 闭包
聊作用域。
返回函数引用了父级的变量,导致无法释放。
牵扯到垃圾回收机制。
# 作用域
作用域是指程序中定义变量的区域,该位置决定了变量的生命周期,也就是变量和函数的可访问范围。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
函数的作用域是在函数调用的时候才决定的。
# 作用域链
当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
# 解释一下调用栈和作用域链的关系
# 题目
如果希望循环中输出123,有哪些方式可以改
for(var i = 0; i < 3; i++){
setTimeout(() => {
console.log(new Date, i);
}, 1000);
}
console.log(new Date,i);
# 变量提升
变量提升(Hoisting)可以将变量和函数在编译阶段放入内存,从而在执行阶段时在声明前使用
在声明变量 a
之前打印变量,控制台输出的结果是 undefined
,而不是预期中的报错 Uncaught ReferenceError: a is not defined
。这就是变量提升。
实际上,JS 并不会移动代码,变量提升和函数提升并不是真正意义上的“提升”,而是解释执行 JS 代码过程所带来的“特性”。
- 对于变量声明如
var a = 3
,会为变量分配内存并初始化为undefined
,赋值语句在生成机器码阶段真正执行代码的时候才进行。 - 对于函数声明如
function sayHello() { console.log('Hello there!') }
,会在内存里创建函数对象,并且直接初始化为该函数对象。
应当注意的是,函数声明的处理优先级要高于变量声明(意味着函数会“提升”到更靠前的位置),同名函数优先了
# 匿名函数声明
基于变量声明和函数声明之间的区别,在实际应用中,使用匿名函数的方式执行声明更不容易产生奇怪的 Bug:
sayHi() // Uncaught TypeError: sayHi is not a functionconsole.log(sayHi) // undefinedvar sayHi = function() { console.log('Hi there!')}sayHi() // Hi there!
使用匿名函数声明时,sayHi
声明发生变量提升,但赋值为 undefined
,因此执行 sayHi()
时会报错 Uncaught TypeError: sayHi is not a function
。随后执行完赋值语句后,才成为一个可以执行的函数变量。
# 为什么要变量提升和函数提升
由于第一批 JS 虚拟机编译器上代码的设计失误,导致了变量在声明之前就被赋予了 undefined
的初始值,产生了变量提升
函数提升,为了解决相互引用
- let 的「创建」过程被提升了,但是初始化没有提升。
- var 的「创建」和「初始化」都被提升了。
- function 的「创建」「初始化」和「赋值」都被提升了。
# new
new
操作符可以帮助我们构建出一个实例,并且绑定上 this
。
只要你在士兵前面使用 new 关键字,那么可以少做四件事情:
- 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
- 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
- 不用 return 临时对象,因为 new 会帮你做;
- 不要给原型想名字了,因为 new 指定名字为 prototype。
new 的作用,就是省那么几行代码。(也就是所谓的语法糖)
new 操作为了记录「临时对象是由哪个函数创建的」,所以预先给「士兵.prototype」加了一个 constructor 属性:
士兵.prototype = {
constructor: 士兵
}
如果你重新对「士兵.prototype」赋值,那么这个 constructor 属性就没了
# 手写
简单版
function Otaku (name, age) {
this.name = name;
this.age = age;
this.habit = 'Games';
}
Otaku.prototype.strength = 60;
Otaku.prototype.sayYourName = function () {
console.log('I am ' + this.name);
}
function objectFactory() {
// 创建新对象
var obj = new Object();
// 获取构造函数
Constructor = [].shift.call(arguments);
// 继承原型链
obj.__proto__ = Constructor.prototype;
// 改变this指向
Constructor.apply(obj, arguments);
return obj;
};
var person = objectFactory(Otaku, 'Kevin', '18')
console.log(person.name) // Kevin
console.log(person.habit) // Games
console.log(person.strength) // 60
person.sayYourName(); // I am Kevin
有返回值的
// 第二版的代码
function objectFactory() {
Constructor = [].shift.call(arguments);
// 原始版
var obj = new Object();
obj.__proto__ = Constructor.prototype;
// es5
var obj = Object.create(Constructor.prototype)
var ret = Constructor.apply(obj, arguments);
return ret instanceof Object ? ret : obj;
};
# call、apply、bind
# 介绍
call、apply都是为了改变函数中的this指向的
区别就是接受参数的方式不太一样
call一个个传,apply传数组
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
bind
MDN的解释是:bind()
方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()
方法的第一个参数作为 this
,传入 bind()
方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
bind只能绑定一次
# 手写
# call
Function.prototype.myCall = function (context) {
// 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
if(typeof this !== 'function'){
throw new TypeError(this + ' is not a function');
}
const ctx = context || window;
ctx.fn = this;
const args = [...arguments].slice(1)
const result = ctx.fn(...args);
delete context.fn;
return result;
}
const obj = {
a: 1
}
function a () {
console.log(this.a)
}
a()
a.myCall(obj)
https://segmentfault.com/a/1190000017206223
# apply
Function.prototype.myApply = function (context, args || []) {
// 1.如果 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。
if(typeof this !== 'function'){
throw new TypeError(this + ' is not a function');
}
const ctx = context || window;
ctx.fn = this;
const result = ctx.fn(...args);
delete context.fn;
return result;
}
# bind
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 空函数隔了一层,改bind后的原型不会影响到bind对象的原型
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
# 原型
JavaScript 中的对象从其他对象继承功能特性。
函数上有个prototype属性,指向其的原型
实例化该函数得到的实例对象,有个__proto__
属性,指向的就是其构造函数的原型。
这么做使得实例对象能获取到构造函数上的属性、方法。
这样一个个串起来就组成了原型链
# class
class
只是原型链的语法糖,与其它语言中的类不是同一样东西。
ES6 中:
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return 'hello, I am ' + this.name;
}
}
var kevin = new Person('Kevin');
kevin.sayHello(); // hello, I am Kevin
对应到 ES5 中就是:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
return 'hello, I am ' + this.name;
};
var kevin = new Person('Kevin');
kevin.sayHello(); // hello, I am Kevin
# 继承
# 原型链继承
function Animal() {
this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
return this.colors
}
function Dog() {}
Dog.prototype = new Animal()
let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors) // ['black', 'white', 'brown']
原型链继承存在的问题:
- 问题1:原型中包含的引用类型属性将被所有实例共享;
- 问题2:子类在实例化的时候不能给父类构造函数传参;
# 借用构造函数实现继承
function Animal(name) {
this.name = name
this.getName = function() {
return this.name
}
}
function Dog(name) {
Animal.call(this, name)
}
Dog.prototype = new Animal()
借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。
# 组合继承
组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function Animal(name) {
this.name = name
this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
return this.name
}
function Dog(name, age) {
Animal.call(this, name)
this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog
let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2)
// { name: "哈赤", colors: ["black", "white"], age: 1 }
# 寄生组合式继承
原型链不用new去继承,而是通过间接创建个函数去继承,从而隔离原型链的共用,只调用了一次父函数
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
稍微封装下上面添加的代码后:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(child, parent) {
let prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
inheritPrototype(Dog, Animal)
复制代码
如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
- Dog.prototype = new Animal()
- Dog.prototype.constructor = Dog
+ Dog.prototype = Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
# class 实现继承
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Dog extends Animal {
constructor(name, age) {
super(name)
this.age = age
}
}
# 存储
# localstorage的会不会出现不同项目的key覆盖别人的key的问题,如何解决
# 模块化
为了方便复用、分类等架构需求,需对代码进行拆分、模块化
# window
不同文件直接往window上挂变量
# IIFE
浏览器环境下,在全局作用域声明的变量都是全局变量。全局变量存在命名冲突、占用内存无法被回收、代码可读性低等诸多问题。
这时,IIFE(匿名立即执行函数)出现了:
;(function () {
...
}());
IIFE的出现,使全局变量的声明数量得到了有效的控制。
# 命名空间
依靠window
对象承载数据的方式是“不可靠”的,如window.config.api
,如果window.config
不存在,则window.config.api
就会报错,所以为了避免这样的错误,代码里会大量的充斥var api = window.config && window.config.api;
这样的代码。
这时,namespace
登场了,简约版本的namespace
函数的实现(只为演示,不要用于生产):
function namespace(tpl, value) {
return tpl.split('.').reduce((pre, curr, i) => {
return (pre[curr] = i === tpl.split('.').length - 1
? (value || pre[curr]) // 不传value就是取值
: (pre[curr] || {}))
}, window);
}
# AMD/CMD
# RequireJS
AMD 是一种异步模块规范,RequireJS 是 AMD 规范的实现。
# 项目目录:
├─ js # js文件夹
│ ├─ ...
│ └─ require.js # RequireJS 的 JS 库
└─ ...
// config.js
define(function() {
var api = 'https://github.com/ronffy';
var config = {
api: api,
};
return config;
});
// utils.js
define(['./config'], function(config) {
var utils = {
request() {
console.log(config.api);
}
};
return utils;
});
// main.js
require(['./utils'], function(utils) {
utils.request();
});
<!-- index.html -->
<!-- ...省略其他 -->
<body>
<script data-main="./js/main" src="./js/require.js"></script>
</body>
</html>
特别说明: 先有 RequireJS,后有 AMD 规范,随着 RequireJS 的推广和普及,AMD 规范才被创建出来。
# CMD
CMD 和 AMD 一样,都是 JS 的模块化规范,也主要应用于浏览器端。 AMD 是 RequireJS 在的推广和普及过程中被创造出来。 CMD 是 SeaJS 在的推广和普及过程中被创造出来。
二者的的主要区别是 CMD 推崇依赖就近,AMD 推崇依赖前置:
# CommonJS
AMD、CMD 主要用于浏览器端,随着 node 诞生,服务器端的模块规范 CommonJS 被创建出来。
// config.js
var api = 'https://github.com/ronffy';
var config = {
api: api,
};
module.exports = config;
// utils.js
var config = require('./config');
var utils = {
request() {
console.log(config.api);
}
};
module.exports = utils;
// main.js
var utils = require('./utils');
utils.request();
console.log(global.api)
# exports和
module.exports
module.exports:
导出的module是个对象,包含exports属性,引入时会自动取module.exports中的值
模块初始化时,exports
和module.exports
指向同一块内存,exports
被重新赋值后,就切断了跟原内存地址的关系。
所以,exports
要这样使用:
// a.js
exports.s = 'i am ronffy';
// b.js
var a = require('./a.js');
console.log(a.s); // i am ronffy
# UMD
!function (root, factory) {
if (typeof exports === 'object' && typeof module === 'object') {
// CommonJS2
module.exports = factory()
// define.amd 用来判断项目是否应用 require.js。
// 更多 define.amd 介绍,请[查看文档](https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property-)
} else if (typeof define === 'function' && define.amd) {
// AMD
define([], factory)
} else if (typeof exports === 'object') {
// CommonJS
exports.myLibName = factory()
} else {
// 全局变量
root.myLibName = factory()
}
}(window, function () {
// 模块初始化要执行的代码
});
就是做环境判断
# ES6 module
# 导出
方式1
export const prefix = 'https://github.com';
export const api = `${prefix}/ronffy`;
方式2
const prefix = 'https://github.com';
const api = `${prefix}/ronffy`;
export {
prefix,
api,
}
方式3(默认导出)
// foo.js
export default function foo() {}
// 等同于:
function foo() {}
export {
foo as default
}
# 导入
方式1
import { api } from './config.js';
// or
// 配合`import`使用的`as`关键字用来为导入的接口重命名。
import { api as myApi } from './config.js';
方式2(整体导入)
import * as config from './config.js';
const api = config.api;
将 config.js 模块导出的所有接口都挂载在config
对象上。
方式3(默认导出的导入)
// foo.js
export const conut = 0;
export default function myFoo() {}
// index.js
// 默认导入的接口此处刻意命名为cusFoo,旨在说明该命名可完全自定义。
import cusFoo, { count } from './foo.js';
// 等同于:
import { default as cusFoo, count } from './foo.js';
export default
导出的接口,可以使用import name from 'module'
导入。这种方式,使导入默认接口很便捷。
方式4(整体加载)
import './config.js';
方式5(动态加载模块)
// 报错
if (/* ... */) {
import { api } from './config.js';
}
// 报错
function foo() {
import { api } from './config.js';
}
// 报错
const modulePath = './utils' + '/api.js';
import modulePath;
使用import()
实现按需加载:
function foo() {
import('./config.js')
.then(({ api }) => {
});
}
const modulePath = './utils' + '/api.js';
import(modulePath);
特别说明: 该功能的提议目前处于 TC39 流程的第4阶段。更多说明,请查看TC39/proposal-dynamic-import (opens new window)。
# CommonJS 和 ES6 module
CommonJS 和 AMD 是运行时加载,在运行时确定模块的依赖关系。
ES6 module 是在编译时(import()
是运行时加载)处理模块依赖关系。
commonjs是值的拷贝,改变导出的值不会引起原值的改变
esm是值的引用,会影响原值
都有做缓存
CommonJS
可以在运行时使用变量进行 require
, 例如 require(path.join('xxxx', 'xxx.js'))
,而静态 import
语法(还有动态 import,返回 Promise)不行,因为 ES6
模块会先解析所有模块再执行代码。
require
会将完整的 exports
对象引入,import
可以只 import
部分必要的内容,这也是为什么使用 Tree Shaking
时必须使用 ES6 模块 的写法。import 另一个模块没有 export
的变量,在代码执行前就会报错,而 CommonJS 是在模块运行时才报错。
# 为什么babel将esm转为commonjs
# __esModule 是什么?干嘛用的?
使用转换工具处理 ES6
模块的时候,常看到打包之后出现 __esModule
属性,字面意思就是将其标记为 ES6 Module
。这个变量存在的作用是为了方便在引用模块的时候加以处理。
例如 ES6
模块中的 export default
在转化成 CommonJS 时会被挂载到 exports['default']
上,当运行 require('./a.js')
时 是不能直接读取到 default 上的值的,为了和 ES6 中 import a from './a.js'
的行为一致,会基于 __esModule
判断处理。
// a.jsexport default 1; // main.jsimport a from './a'; console.log(a);
转化后
// a.jsObject.defineProperty(exports, "__esModule", { value: true});exports.default = 1; // main.js'use strict'; var _a = require('./a'); var _a2 = _interopRequireDefault(_a); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log(_a2.default);
a 模块 export defualt
会被转换成 exports.default = 1
;,这也是平时前端项目开发中使用 require
为什么还常常需要 .default
才能取到目标值的原因。
接着当运行 import a from './a.js'
时,es module
预期的是返回 export 的内容。工具会将代码转换为 _interopRequireDefault
包裹,在里面判断是否为 esModule,是的话直接返回,如果是 commonjs
模块的话则包裹一层 {default: obj}
,最后获取 a
的值时,也会被装换成 _a1.default
。
# Promise
promise 有 3 个状态,分别是 pending, fulfilled 和 rejected。
在 pending 状态,promise 可以切换到 fulfilled 或 rejected。
在 fulfilled 状态,不能迁移到其它状态,必须有个不可变的 value。
在 rejected 状态,不能迁移到其它状态,必须有个不可变的 reason。
# 简单版
function MyPromise (fn) {
// 回调函数集
this.cbs = [];
// 传入的fn执行回调
const resolve = (value) => {
setTimeout(() => this.cbs.forEach((cb) => cb(value)));
}
// 调用fn
fn(resolve)
}
MyPromise.prototype.then = function (onResolve) {
// 为了能链式调用then
return new MyPromise((resolve) => {
this.cbs.push((value) => {
const res = onResolve(value)
// 解决then里返回的是promise
if (res instanceof MyPromise) {
res.then(resolve)
} else {
resolve(res)
}
})
})
}
new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 500);
})
.then((res) => {
console.log(res);
return new MyPromise((resolve) => {
setTimeout(() => {
resolve(2);
}, 1000);
});
})
.then(console.log);
# 完整版
// 判断变量否为function
const isFunction = variable => typeof variable === 'function'
// 定义Promise的三种状态常量
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor (handle) {
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
// 添加状态
this._status = PENDING
// 添加状态
this._value = undefined
// 添加成功回调函数队列
this._fulfilledQueues = []
// 添加失败回调函数队列
this._rejectedQueues = []
// 执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 添加resovle时执行的函数
_resolve (val) {
const run = () => {
if (this._status !== PENDING) return
// 依次执行成功队列中的函数,并清空队列
const runFulfilled = (value) => {
this._fulfilledQueues.forEach((item) => {
item(value)
})
this._fulfilledQueues = []
// let cb
// while (cb = this._fulfilledQueues.shift()) {
// cb(value)
// }
}
// 依次执行失败队列中的函数,并清空队列
const runRejected = (error) => {
this._rejectedQueues.forEach((item) => {
item(error)
})
this._rejectedQueues = []
// let cb
// while (cb = this._rejectedQueues.shift()) {
// cb(error)
// }
}
/* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态
*/
if (val instanceof MyPromise) {
val.then(value => {
this._value = value
this._status = FULFILLED
runFulfilled(value)
}, err => {
this._value = err
this._status = REJECTED
runRejected(err)
})
} else {
this._value = val
this._status = FULFILLED
runFulfilled(val)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加reject时执行的函数
_reject (err) {
if (this._status !== PENDING) return
// 依次执行失败队列中的函数,并清空队列
const run = () => {
this._status = REJECTED
this._value = err
this._rejectedQueues.forEach((item) => {
item(err)
})
this._rejectedQueues = []
// let cb
// while (cb = this._rejectedQueues.shift()) {
// cb(err)
// }
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加then方法
then (onFulfilled, onRejected) {
const {_value, _status} = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value)
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
// 否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的P romise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error)
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
// 否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
// 添加catch方法
catch (onRejected) {
return this.then(undefined, onRejected)
}
// 添加静态resolve方法
static resolve (value) {
// 如果参数是MyPromise实例,直接返回这个实例
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
// 添加静态reject方法
static reject (value) {
return new MyPromise((resolve, reject) => reject(value))
}
// 添加静态all方法
static all (list) {
return new MyPromise((resolve, reject) => {
/**
* 返回值的集合
*/
let values = []
let count = 0
for (let [i, p] of list.entries()) {
// 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === list.length) resolve(values)
}, err => {
// 有一个被rejected时返回的MyPromise状态就变成rejected
reject(err)
})
}
})
}
// 添加静态race方法
static race (list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
this.resolve(p).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
finally (cb) {
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason => MyPromise.resolve(cb()).then(() => {
throw reason
})
)
}
}
export default MyPromise
# 超时控制
加个定时器用promise包起来,再race即可
# catch原理
# then的链式调用,返回的一个新的promise的状态是什么
# 如何限制 Promise 请求并发数
# 题目
有reject就会走reject
const promise1 = Promise.resolve("First");
const promise2 = Promise.resolve("Second");
const promise3 = Promise.reject("Third");
const promise4 = Promise.resolve("Fourth");
const runPromises = async () => {
const res1 = await Promise.all([promise1, promise2]);
const res2 = await Promise.all([promise3, promise4]);
return [res1, res2];
};
runPromises()
.then((res) => console.log(res))
.catch((err) => console.log(err));
Third
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
# 迭代器与生成器
生成器是一种可以用来控制迭代器(iterator)的函数,它可以随时暂停,并可以在任意时候恢复。
function * generatorForLoop(num) {
for (let i = 0; i < num; i += 1) {
yield console.log(i);
}
}
const genForLoop = generatorForLoop(5);
genForLoop.next(); // 首先 console.log - 0
genForLoop.next(); // 1
genForLoop.next(); // 2
genForLoop.next(); // 3
genForLoop.next(); // 4
# async await
async函数是generator函数的语法糖
# 手写
就是用promise包一下,然后每个yield结束后递归调用下一步,结束时resolve。
/**
* async的执行原理
* 其实就是自动执行generator函数
* 暂时不考虑genertor的编译步骤(更复杂)
*/
const getData = () =>
new Promise(resolve => setTimeout(() => resolve("data"), 1000))
// 这样的一个async函数 应该再1秒后打印data
async function test() {
const data = await getData()
console.log(data)
return data
}
// async函数会被编译成generator函数 (babel会编译成更本质的形态,这里我们直接用generator)
function* testG() {
// await被编译成了yield
const data = yield getData()
console.log('data: ', data);
const data2 = yield getData()
console.log('data2: ', data2);
return data + '123'
}
function asyncToGenerator(generatorFunc) {
return function() {
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve, reject) => {
function step(key, arg) {
let generatorResult
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
const { value, done } = generatorResult
if (done) {
return resolve(value)
} else {
return Promise.resolve(value).then(
function onResolve(val) {
step("next", val)
},
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}
const testGAsync = asyncToGenerator(testG)
testGAsync().then(result => {
console.log(result)
})
# 事件循环
# 同步异步
js是单线程
不同任务耗时不一样,一直等着一个太傻了。所以分成了
同步任务
异步任务
像网页渲染就是同步任务,下载图片音乐之类的就是异步的
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
主线程会不断检测是否为空,空了就取Event Queue
# 宏任务微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout
和setInterval
会进入相同的Event Queue。
# 题目
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 1,7,6,8,2,4,3,5,9,11,10,12
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise((resolve) => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");
script start
VM277:2 async1 start
VM277:7 async2
VM277:15 promise1
VM277:20 script end
VM277:4 async1 end
VM277:18 promise2
undefined
VM277:11 setTimeout
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
throw new Error();
}, 2000);
});
const p2 = p1
.then((val) => {
console.log(val);
return val + 1;
})
.catch((err) => {
console.log(err);
return err;
});
Promise.all([p2, Promise.reject(3)])
.then((val2) => {
console.log(val2);
})
.catch((err2) => {
console.log(err2);
});
# 节流防抖
# 节流
在一定时间内执行一次
throttle
会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
//休息时间 暂不接客
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn()
valid = true;
}, delay)
}
}
/* 请注意,节流函数并不止上面这种实现方案,
例如可以完全不借助setTimeout,可以把状态位换成时间戳,然后利用时间戳差值是否大于指定间隔时间来做判定。
也可以直接将setTimeout的返回的标记当做判断条件-判断当前定时器是否存在,如果存在表示还在冷却,并且在执行fn之后消除定时器表示激活,原理都一样
*/
# 防抖
在一定时间内不再触发该事件,则执行一次
function debounce(fn,delay){
let timer = null //借助闭包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(fn,delay) // 简化写法
}
}
支持立即执行、取消、返回结果
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
# 应用场景举例
- 搜索框input事件,例如要支持输入实时搜索可以使用节流方案(间隔一段时间就必须查询相关内容),或者实现输入间隔大于某个值(如500ms),就当做用户输入完成,然后开始搜索,具体使用哪种方案要看业务需求。
- 页面resize事件,常见于需要做页面适配的时候。需要根据最终呈现的页面情况进行dom渲染(这种情形一般是使用防抖,因为只需要判断最后一次的变化情况)
# 柯里化
将函数中的功能剥离成单独的函数,可以方便复用重复参数逻辑
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
可以看成是将之前的参数存起来了,下次调用的时候一起拼上使用
# 偏函数
什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:
function add(a, b, c) {
return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
复制代码
发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:
function partial(fn, ...args) {
return (...arg) => {
return fn(...args, ...arg)
}
}
# 垃圾回收
# 前言
在JavaScript中,当我们创建变量(对象,字符串等)的时候,系统会自动给对象分配对应的内存。
数据类型分为两类,简单类型和引用类型,对于简单类型,内存是保存在栈(stack)空间中,复杂数据类型,内存是保存在堆(heap)空间中。
对于栈的内存空间,只保存简单数据类型的内存,由操作系统自动分配和自动释放。而堆空间中的内存,由于大小不固定,系统无法无法进行自动释放,这个时候就需要JS引擎来手动的释放这些内存。
# 为什么需要垃圾回收
在Chrome中,v8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限制呢?
- 表层原因是,V8最初为浏览器而设计,不太可能遇到用大量内存的场景
- 深层原因是,V8的垃圾回收机制的限制(如果清理大量的内存垃圾是很耗时间,这样回引起JavaScript线程暂停执行的时间,那么性能和应用直线下降)
前面说到栈内的内存,操作系统会自动进行内存分配和内存释放,而堆中的内存,由JS引擎(如Chrome的V8)手动进行释放,当我们的代码没有按照正确的写法时,会使得JS引擎的垃圾回收机制无法正确的对内存进行释放(内存泄露),从而使得浏览器占用的内存不断增加,进而导致JavaScript和应用、操作系统性能下降。
# Chrome 垃圾回收算法
在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在经过一次的垃圾回收之后,内存就会被释放掉,而少部分的对象存活周期将会很长,一直是活跃的对象,不需要被回收。为了提高回收效率,V8 将堆分为两类新生代
和老生代
,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
# 新生代垃圾回收器 - Scavenge
Scavange算法将新生代堆分为两部分,分别叫from-space
和to-space
,工作方式也很简单,就是将from-space
中存活的活动对象复制到to-space
中,并将这些对象的内存有序的排列起来,然后将from-space
中的非活动对象的内存进行释放,完成之后,将from space
和to space
进行互换,这样可以使得新生代中的这两块区域可以重复利用。
简单的描述就是:
- 标记活动对象和非活动对象
- 复制 from space 的活动对象到 to space 并对其进行排序
- 释放 from space 中的非活动对象的内存
- 将 from space 和 to space 角色互换
怎么知道哪些对象是活动对象和非活动对象的呢?
有一个概念叫对象的可达性,表示从初始的根对象(window,global)的指针开始,这个根指针对象被称为根集(root set),从这个根集向下搜索其子节点,被搜索到的子节点说明该节点的引用对象可达,并为其留下标记,然后递归这个搜索的过程,直到所有子节点都被遍历结束,那么没有被标记的对象节点,说明该对象没有被任何地方引用,可以证明这是一个需要被释放内存的对象,可以被垃圾回收器回收。
新生代中的对象什么时候变成老生代的对象呢?
在新生代中,还进一步进行了细分,分为nursery
子代和intermediate
子代两个区域,一个对象第一次分配内存时会被分配到新生代中的nursery
子代,如果进过下一次垃圾回收这个对象还存在新生代中,这时候我们移动到 intermediate
子代,再经过下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升。
# 老生代垃圾回收 - Mark-Sweep & Mark-Compact
新生代空间中的对象满足一定条件后,晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大,如果这个时候再使用scavenge
算法的话,会出现两个问题:
- scavenge为复制算法,重复复制活动对象会使得效率低下
- scavenge是牺牲空间来换取时间效率的算法,而老生代支持的容量较大,会出现空间资源浪费问题
所以在老生代空间中采用了 Mark-Sweep(标记清除) 和 Mark-Compact(标记整理) 算法。
# Mark-Sweep
- 标记阶段:对老生代进行第一次扫描,标记活动对象
- 清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象
一个问题,被清除的对象遍布于各内存地址,产生很多内存碎片。
# Mark-Compact
为了解决内存碎片问题,Mark-Compact被提出,它是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。
# 数组
# 扁平化
递归
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
} else {
result.push(arr[i])
}
}
return result;
}
es6 展开运算符
每次去掉一级的数组
const flatten = (list, level = +Infinity) => {
let res = list
for (let i = 0; i < level; i++) {
res = [].concat(...res)
if (!res.some(item => Array.isArray(item))) break
}
return res
};
# reduce
Array.prototype.reduce2 = function(callback, initialValue) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0, acc
if (arguments.length > 1) {
acc = initialValue
} else {
// 没传入初始值的时候,取数组中第一个非 empty 的值为初始值
while (k < len && !(k in O)) {
k++
}
if (k > len) {
throw new TypeError( 'Reduce of empty array with no initial value' );
}
acc = O[k++]
}
while (k < len) {
if (k in O) {
acc = callback(acc, O[k], k, O)
}
k++
}
return acc
}
# 去重
indexof
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}
Set
var unique = arr => [...new Set(arr)]
两层for循环,删除重复的
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
j--;
}
}
}
return arr;
}
sort
排序后,判断跟前面那个一不一样,不一样就推入新数组
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return;
}
arr = arr.sort()
var arrry= [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
arrry.push(arr[i]);
}
}
return arrry;
}
用对象或map记录,不存在就推入新数组
includes
不存在新数组中就推入新数组
利用reduce+includes
function unique(arr){
return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
# forEach
Array.prototype.forEach2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this) // this 就是当前的数组
const len = O.length >>> 0 // 后面有解释
let k = 0
while (k < len) {
if (k in O) {
callback.call(thisArg, O[k], k, O);
}
k++;
}
}
# map
基于 forEach 的实现能够很容易写出 map 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.map2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}
+ return res
}
# filter
同样,基于 forEach 的实现能够很容易写出 filter 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.filter2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
- let k = 0
+ let k = 0, res = []
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ res.push(O[k])
+ }
}
k++;
}
+ return res
}
# some
同样,基于 forEach 的实现能够很容易写出 some 的实现:
- Array.prototype.forEach2 = function(callback, thisArg) {
+ Array.prototype.some2 = function(callback, thisArg) {
if (this == null) {
throw new TypeError('this is null or not defined')
}
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function')
}
const O = Object(this)
const len = O.length >>> 0
let k = 0
while (k < len) {
if (k in O) {
- callback.call(thisArg, O[k], k, O);
+ if (callback.call(thisArg, O[k], k, O)) {
+ return true
+ }
}
k++;
}
+ return false
}
# 跨域8种解决方案
iframe + document.domain location.hash window.name等三种
postMessage
nodejs 中间件,vue脚手架中就用了http-proxy-middleware
cors 最简单
websocket
jsonp
这是比较早的解决方案,我们的script标签的src还是img标签的src,或者说link标签的href他们没有被同源策略所限制。它就是通过通过 script 标签加载并执行其他的域的事件之类。
缺点:只支持 get 和 http 请求;在请求完毕后可以通过调用 callback 的方式返回结果,如果请求失败,不会返回状态码。
const jsonp = ({ url, params, callbackName }) => { const generateUrl = () => { let dataSrc = '' for (let key in params) { if (params.hasOwnProperty(key)) { dataSrc += `${key}=${params[key]}&` } } dataSrc += `callback=${callbackName}` return `${url}?${dataSrc}` } return new Promise((resolve, reject) => { const scriptEle = document.createElement('script') scriptEle.src = generateUrl() document.body.appendChild(scriptEle) window[callbackName] = data => { resolve(data) document.removeChild(scriptEle) } }) }
nginx反向代理
https://juejin.cn/post/6999977495550394404
# 普通函数 箭头函数的区别
- 1.箭头函数没有原型 原型是undefined
- 2.箭头函数this指向全局对象 而函数指向引用对象
- 3.call,apply,bind方法改变不了箭头函数的指向
- 4.箭头函数不能做构造函数
# axios fetch ajax区别
https://www.jianshu.com/p/8bc48f8fde75
# 用什么方式发请求,axios 是个同构的工具,它是如何实现区分 Node 和浏览器环境的
# axios 内部如何把 xhr 的 callback 转换为 promise 的,如何处理请求异常的
# 前端错误监控
# 性能优化
巴拉巴拉 从代码 网络 资源加载 打包部署 等等层面去简单阐述 性能优化可说的地方太多
社区里就有很多优秀的性能优化文章(大佬),建议收藏阅读
1.前端性能优化 24 条建议(2020) (opens new window)
2.写给中高级前端关于性能优化的9大策略和6大指标 | 网易四年实践 (opens new window)
3.聊一聊前端性能优化 (opens new window)
4.Vue 项目性能优化 — 实践指南(网上最全 / 详细) (opens new window)
思考:用户体验优化 比如白屏加载问题(骨架屏)
# 骨架屏
# 懒加载
# 一个页面的性能指标都有哪些,你是如何做监控的
# 错误处理
# 对线上各类异常如何处理,对线上的静态资源加载失败如何捕获
# 鉴权
jwt 如何实现踢人,session 和 jwt 鉴权的区别
# 各种手写
# eventbus手动实现
如何自己实现一个 EventBus (opens new window)
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'
# 解析 URL 参数为对象
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
# 字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
复制代码
测试:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12
}
render(template, person); // 我是布兰,年龄12,性别undefined
# 图片懒加载
与普通的图片懒加载不同,如下这个多做了 2 个精心处理:
- 图片全部加载完成后移除事件监听;
- 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
// 修正错误,需要加上自执行
- const imgLazyLoad = function() {
+ const imgLazyLoad = (function() {
let count = 0
return function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
deleteIndexList.push(index)
count++
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad)
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
}
- }
+ })()
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
# AJAX
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
# 实现 Object.assign
Object.assign2 = function(target, ...source) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target)
source.forEach(function(obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}
# setInterval
function mySetInterval(fn, delay, ...args) {
const timer = { flag: true }
function interval() {
if (timer.flag) {
fn(...args)
setTimeout(() => {
interval()
}, delay)
}
}
setTimeout(() => {
interval()
}, delay)
return timer
}
# 写一个发布订阅模式的on/emit/off
如果需要把订阅者执行成功和失败的方法分开,需要怎么做 如果希望失败的可追溯,找到是哪个订阅者的报错,需要怎么做 实现一下before和after方法,可以添加一些前置的和后置的订阅者 现在希望给所有的订阅者加打点上报的功能,并且提供全局的开关,需要如何设计 如果需要给某一个订阅者单独加一个打点,需要如何设计
# 如果想给一个对象上的所有方法在执行时加一些打点上报的功能,如何做
# a==1&&a==2有什么方式让它返回true
# 判断一个对象是否是循环引用对象
# vue
# 优缺点
# 优点
渐进式
从只引入个vue文件,到脚手架、路由、状态管理等功能,可满足渐进式的开发需求。
组件化
可以方便的开发组件,也有各种组件库可以引入。
轻量级
虚拟dom
可以减少对dom的操作
方便跨平台兼容
响应式
不像用jq那要既要改数据又要改视图了,通过obj.defi可以监听数据的变化,自动更新视图
单页面路由
通过在一个页面上切换显示内容来实现切换页面的功能,比以前切换html更快
# 缺点
单页面不利于seo
页面内容是动态加载的,爬虫获取不到。
所以存在服务端渲染
不支持IE8以下
obj.defi 不支持
vue3换了proxy后直接不支持所有ie了
首屏加载时间长
所有需要的内容会在首屏加载,导致第一屏出现时过慢
解决办法:骨架屏(体验上优化),去除不需要的依赖,服务端渲染
# MVVM是什么?和MVC有何区别呢?
MVC
- Model(模型):负责从数据库中取数据
- View(视图):负责展示数据的地方
- Controller(控制器):用户交互的地方,例如点击事件等等
- 思想:Controller将Model的数据展示在View上
MVVM
- VM:也就是View-Model,做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
- 思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
区别
整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
Vue是不是MVVM框架?
Vue是MVVM框架,但是不是严格符合MVVM,因为MVVM规定Model和View不能直接通信,而Vue的ref
可以做到这点
# Vue和JQuery的区别在哪?为什么放弃JQuery用Vue?
- 1.jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据即可
- 2.在操作DOM频繁的场景里,jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能
- 3.Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上
- 4.Vue集成的一些库,大大提高开发效率,比如Vuex,Router等
# 为什么data是个函数并且返回一个对象呢?
data
之所以只一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数
并返回新的数据对象,这样,可以避免多处调用之间的数据污染
。
之所以根实例可以不是函数,是因为,只有一个实例化。不会出现多个实例共享引用同一个数据对象的现象。
而其他组件,可能会被多次使用,是可能多次实例化。
# 修饰符
# 内部指令
# 组件之间的传值方式有哪些
父组件传值给子组件,子组件使用props
进行接收
子组件传值给父组件,子组件使用$emit+事件
对父组件进行传值
组件中可以使用$parent
和$children
获取到父组件实例和子组件实例,进而获取数据
使用$attrs
和$listeners
,在对一些组件进行二次封装时可以方便传值,例如A->B->C
使用$refs
获取组件实例,进而获取数据
使用Vuex
进行状态管理
使用eventBus
进行跨组件触发事件,进而传递数据
使用provide
和inject
,官方建议我们不要用这个,我在看ElementUI
源码时发现大量使用
使用浏览器本地缓存,例如localStorage
# 如何设置动态class,动态style?
- 动态class对象:
<div :class="{ 'is-active': true, 'red': isRed }"></div>
- 动态class数组:
<div :class="['is-active', isRed ? 'red' : '' ]"></div>
- 动态style对象:
<div :style="{ color: textColor, fontSize: '18px' }"></div>
- 动态style数组:
<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
# v-if和v-show有何区别?
- 1.
v-if
是通过控制dom元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期,因为显隐决定了组件的生成和销毁 - 2.
v-show
是通过控制dom元素的css样式来实现显隐,不会销毁 - 3.频繁或者大数量显隐使用
v-show
,否则使用v-if
# computed和watch有何区别?
1.computed
是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量
凑在一起计算出一个变量
,并且computed
具有缓存机制
,依赖值不变的情况下其会直接读取缓存进行复用,computed
不能进行异步操作
2.watch
是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量
的变化决定多个变量
的变化,watch
可以进行异步操作
3.简单记就是:一般情况下computed
是多对一
,watch
是一对多
https://www.cnblogs.com/tugenhua0707/p/11760466.html
根据computed中的key来实例化watcher
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
# 生命周期
# 为什么v-if和v-for不建议用在同一标签
v-for优先级更高,会先渲染再if判断,浪费性能
可以用computed先计算好需要渲染的数据
# 不需要响应式的数据应该怎么处理?
// 方法一:将数据定义在data之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
# watch有哪些属性,分别有什么用?
当我们监听一个基本数据类型时:
watch: {
value () {
// do something
}
}
当我们监听一个引用数据类型时:
watch: {
obj: {
handler () { // 执行回调
// do something
},
deep: true, // 是否进行深度监听
immediate: true // 是否初始执行handler函数
}
}
# Vue组件生命周期有哪些?
- beforeCreate 在实例初始化之后,数据观测observer 和event、watcher事件配置之前被调用
- created 实例已经创建完成,在这一步,以下配置被完成
- 数据观测
- 属性和方法的运算
- watch/event时间回调
- $el尚未生成
- beforeMount 在挂载之前被调用,render尚未被调用
- mounted el被新创建的vm.$el替换,并挂载到实例上去之后调用
- beforeUpdate 数据更新时,被调用,发生在虚拟Dom重新渲染和打补丁之前
- update 由于数据更改导致的虚拟Dom重新渲染和打补丁,在这之后调用
- beforeDestroy 实例销毁之前调用
- destroyed 实例销毁之后调用,调用后Vue实例的所有东西都会被解绑,所有的事件监听会被移除,子实例被销毁,该钩子在服务端渲染期间不被调用
- keep-alive(activated & deactivated)
# updated 什么情况下会触发
# beforeCreate 的时候能拿到 Vue 实例么
# 组件销毁的时候调用的是哪个 API
# 什么情况下会触发组件销毁,销毁的时候会卸载自定义事件和原生事件么
# 父子组件生命周期顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
# Vue生命周期钩子是如何实现的?
Vue的生命周期钩子是回调函数,当创建组件实例的过程中会调用相应的钩子方法。 内部会对钩子进行处理,将钩子函数维护成数组的形式。
# vue.mixin的使用场景和原理?
Vue的mixin的作用就是抽离公共的业务逻辑,原理类似对象的继承,当组件初始化的时候,会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。 如果混入的数据和本身组件的数据有冲突,采用本身的数据为准。
缺点:命名冲突、数据来源不清晰
# 对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?
- 原因:
Object.defineProperty
没有对对象的新属性进行属性劫持 - 对象新属性无法更新视图:使用
Vue.$set(obj, key, value)
,组件中this.$set(obj, key, value)
- 删除属性无法更新视图:使用
Vue.$delete(obj, key)
,组件中this.$delete(obj, key)
# 直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?
- 原因:Vue没有对数组进行
Object.defineProperty
的属性劫持,所以直接arr[index] = xxx是无法更新视图的 - 使用数组的splice方法,
arr.splice(index, 1, item)
- 使用
Vue.$set(arr, index, value)
# 自定义指令
- bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
- inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
- update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
- componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
- unbind: 只调用一次, 指令与元素解绑时调用。
# 怎么理解 vue 单向数据流的
# 自定义 v-modal
# 插槽的使用以及原理
# 类型
默认的
具名的
作用域的
# 原理
父组件解析出scopeSlots,里面根据插槽名,给出内容的函数
子组件解析时,根据插槽名字去取函数来生成内容
# 为什么不建议用index、随机数做key?
dom diff时会根据key去判断是不是同一个节点,相同则可以不渲染。
用index的话,数据更新后,index代表的数据就变了,会让所有dom都去更新
用唯一的key就可以只更新需要更新的
为什么 Vue 中不要用 index 作为 key?(diff 算法详解) (opens new window)
# nextTick
# 用处
同一事件循环内
多次修改,会统一
进行一次视图更新
- Vue是
异步更新
,所以数据一更新,视图却还没更新,nexttick保证在渲染后获取到正确数据
# vue源码
let callbacks = []; //回调函数
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //先采用微任务并按照优先级优雅降级的方式实现异步刷新
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
timerFunc();
}
}
# npm上插件的源码
'use strict';
var ensureCallable = function (fn) {
if (typeof fn !== 'function') throw new TypeError(fn + " is not a function");
return fn;
};
var byObserver = function (Observer) {
var node = document.createTextNode(''), queue, currentQueue, i = 0;
new Observer(function () {
var callback;
if (!queue) {
if (!currentQueue) return;
queue = currentQueue;
} else if (currentQueue) {
queue = currentQueue.concat(queue);
}
currentQueue = queue;
queue = null;
if (typeof currentQueue === 'function') {
callback = currentQueue;
currentQueue = null;
callback();
return;
}
node.data = (i = ++i % 2); // Invoke other batch, to handle leftover callbacks in case of crash
while (currentQueue) {
callback = currentQueue.shift();
if (!currentQueue.length) currentQueue = null;
callback();
}
}).observe(node, { characterData: true });
return function (fn) {
ensureCallable(fn);
if (queue) {
if (typeof queue === 'function') queue = [queue, fn];
else queue.push(fn);
return;
}
queue = fn;
node.data = (i = ++i % 2);
};
};
module.exports = (function () {
// Node.js
if ((typeof process === 'object') && process && (typeof process.nextTick === 'function')) {
return process.nextTick;
}
// queueMicrotask
if (typeof queueMicrotask === "function") {
return function (cb) { queueMicrotask(ensureCallable(cb)); };
}
// MutationObserver
if ((typeof document === 'object') && document) {
if (typeof MutationObserver === 'function') return byObserver(MutationObserver);
if (typeof WebKitMutationObserver === 'function') return byObserver(WebKitMutationObserver);
}
// W3C Draft
// http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/setImmediate/Overview.html
if (typeof setImmediate === 'function') {
return function (cb) { setImmediate(ensureCallable(cb)); };
}
// Wide available standard
if ((typeof setTimeout === 'function') || (typeof setTimeout === 'object')) {
return function (cb) { setTimeout(ensureCallable(cb), 0); };
}
return null;
}());
# 异步更新 DOM 这个操作,Vue 和 React 都是如何实现的,Vue 的异步处理还有其他方式可以做么,除了 MessageChannel 还有其他和他用法类似的 API 么
# SSR是什么?有什么好处?
SSR
就是服务端渲染- 基于
nodejs serve
服务环境开发,所有html
代码在服务端渲染 - 数据返回给前端,然后前端进行“激活”,即可成为浏览器识别的html代码
SSR
首次加载更快,有更好的用户体验,有更好的seo优化,因为爬虫能看到整个页面的内容,如果是vue项目,由于数据还要经过解析,这就造成爬虫并不会等待你的数据加载完成,所以其实Vue项目的seo体验并不是很好
next.js
nuxt.js
#
# Vue.set方法的原理?
function set(target, key, val) {
// 判断是否是数组
if (Array.isArray(target)) {
// 判断谁大谁小
target.length = Math.max(target.length, key)
// 执行splice
target.splice(key, 1, val)
return val
}
const ob = target.__ob__
// 如果此对象没有不是响应式对象,直接设置并返回
if (key in target && !(key in target.prototype) || !ob) {
target[key] = val
return val
}
// 否则,新增属性,并响应式处理
defineReactive(target, key, val)
return val
}
# Vue.delete方法的原理?
function del (target, key) {
// 判断是否为数组
if (Array.isArray(target)) {
// 执行splice
target.splice(key, 1)
return
}
const ob = target.__ob__
// 对象本身就没有这个属性,直接返回
if (!(key in target)) return
// 否则,删除这个属性
delete target[key]
// 判断是否是响应式对象,不是的话,直接返回
if (!ob) return
// 是的话,删除后要通知视图更新
ob.dep.notify()
}
# 如果子组件改变props里的数据会发生什么
如果修改的是基本类型,则会报错
引用类型不报错,并且父组件的数据会跟着变
# props怎么自定义验证
props: {
num: {
default: 1,
validator: function (value) {
// 返回值为false则验证不通过,报错
return [
1, 2, 3, 4, 5
].indexOf(value) !== -1
}
}
}
# watch的immediate属性有什么用?
初始化组件的时候就会执行一次
# watch监听一个对象时,如何排除某些属性的监听
这种方法有待商榷,用computed重新获取一个过滤后的去监听会不会更容易理解
mounted() {
Object.keys(this.params)
.filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
.forEach((_) => {
this.$watch((vm) => vm.params[_], handler, {
deep: true,
});
});
},
data() {
return {
params: {
a: 1,
b: 2,
c: 3,
d: 4
},
};
},
watch: {
params: {
deep: true,
handler() {
this.getList;
},
},
}
# 审查元素时发现data-v-xxxxx,这是啥?
这是在标记vue文件中css时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记,所以每引入一个component就会出现一个新的'data-v-xxx'标记
# computed如何实现传参?
看起来很恶心,用method更容易理解吧
// html
<div>{{ total(3) }}</div>
// js
computed: {
total() {
return function(n) {
return n * this.num
}
},
}
# hook的使用
将变量放在一处,方便管理
export default{
methods:{
fn(){
let timer = setInterval(()=>{
//具体执行代码
console.log('1');
},1000);
this.$once('hook:beforeDestroy',()=>{
clearInterval(timer);
timer = null;
})
}
}
}
//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},
# provide和inject是响应式的吗?
传引用类型就是响应式的,普通类型不行
直接传this
# Vue的el属性和$mount优先级?
new Vue({
router,
store,
el: '#app',
render: h => h(App)
}).$mount('#ggg')
# 动态指令和参数使用过吗?
<template>
...
<aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
...
data(){
return{
...
someEvent: someCondition ? "click" : "dbclick",
someProps: someCondition ? "num" : "price"
}
},
methods: {
handleSomeEvent(){
// handle some event
}
}
</script>
# 相同的路由组件如何重新渲染?
const routes = [
{
path: "/a",
component: MyComponent
},
{
path: "/b",
component: MyComponent
},
];
如果依然想重新渲染,怎么办呢?可以使用
key
<template>
<router-view :key="$route.path"></router-view>
</template>
# 自定义v-model
export default: {
model: {
event: 'change',
prop: 'checked'
}
}
原理:
# 如何将获取data中某一个数据的初始状态?
data() {
return {
num: 10
},
mounted() {
this.num = 1000
},
methods: {
howMuch() {
// 计算出num增加了多少,那就是1000 - 初始值
// 可以通过this.$options.data().xxx来获取初始值
console.log(1000 - this.$options.data().num)
}
}
这里也可以看出,data要用函数,不然会被直接改变
# Vue为什么要用虚拟Dom
- 虚拟dom就是用js对象来描述真实Dom,是对真实Dom的抽象
- 由于直接操作Dom性能低,但是js层的操作效率高,可以将Dom操作转化成对象操作。最终通过diff算法比对差异进行更新Dom
- 虚拟Dom不依赖真实平台环境,可以实现跨平台
# vue初始化流程
https://ustbhuangyi.github.io/vue-analysis/
# new vue
// index.js
// 实例一个Vue对象
let vue = new Vue({
props: {},
data() {
return {
a: 1,
b: [1],
c: { d: 1 }
}
},
watch: {},
render: () => {}
})
# 对options对象的初始化
// index.js
const { initMixin } = require('./init')
function Vue(options) {
// 初始化传进来的options配置
this._init(options)
}
// 配置Vue构造函数的_init方法
// 这么做有利于代码分割
initMixin(Vue)
// init.js
const { initState } = require('./state')
function initMixin(Vue) {
// 在Vue的原型上挂载_init函数
Vue.prototype._init = function (options) {
// vm变量赋值为Vue实例
const vm = this
// 将传进来的options对象赋值给vm上的$options变量
vm.$options = options
// 执行初始化状态函数
initState(vm)
}
}
module.exports = {
initMixin: initMixin
}
initState:总初始化函数,初始化props,data,watch,methods,computed
等
initData:初始化data
的函数
proxy:代理函数,主要作用是this.data.xxx的读写可以直接this.xxx实现,少去中间的data
const { observe } = require('./observer/index')
function initState(vm) {
// 获取vm上的$options对象,也就是options配置对象
const opts = vm.$options
if (opts.props) {
initProps(vm)
}
if (opts.methods) {
initMethods(vm)
}
if (opts.data) {
// 如有有options里有data,则初始化data
initData(vm)
}
if (opts.computed) {
initComputed(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
// 初始化data的函数
function initData(vm) {
// 获取options对象里的data
let data = vm.$options.data
// 判断data是否为函数,是函数就执行(注意this指向vm),否则就直接赋值给vm上的_data
// 这里建议data应为一个函数,return 一个 {},这样做的好处是防止组件的变量污染
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 为data上的每个数据都进行代理
// 这样做的好处就是,this.data.a可以直接this.a就可以访问了
for (let key in data) {
proxy(vm, '_data', key)
}
// 对data里的数据进行响应式处理
// 重头戏
observe(data)
}
// 数据代理
function proxy(object, sourceData, key) {
Object.defineProperty(object, key, {
// 比如本来需要this.data.a才能获取到a的数据
// 这么做之后,this.a就可以获取到a的数据了
get() {
return object[sourceData][key]
},
// 比如本来需要this.data.a = 1才能修改a的数据
// 这么做之后,this.a = 1就能修改a的数据了
set(newVal) {
object[sourceData][key] = newVal
}
})
}
module.exports = { initState: initState }
# vue响应式原理
整体思路是数据劫持+观察者模式
对象内部通过defineReactive
方法,使用 Object.defineProperty
将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的dep
属性,存放他所依赖的watcher
(依赖收集),当属性变化后会通知自己对应的watcher
去更新(派发更新)。
- Observer:观察者对象,对
对象或数组
进行响应式处理的地方- defineReactive:拦截对象上每一个
key
的get与set函数
的地方- observe:响应式处理的入口
流程大概是这样:observe -> Observer -> defineReactive -> observe -> Observer -> defineReactive 递归
const { arrayMethods } = require('./array')
class Observer {
constructor(value) {
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
})
if(Array.isArray(value)) {
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(data) {
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = data[key]
defineReactive(data, key, value)
}
}
observeArray(items) {
for(let i = 0; i < items.length; i++) {
observe(items[i])
}
}
}
function defineReactive(data, key, value) {
const childOb = observe(value)
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
console.log('获取值')
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set(newVal) {
if (newVal === value) return
observe(newVal)
value = newVal
dep.notify()
}
})
}
function observe(value) {
if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) {
return new Observer(value)
}
}
function dependArray(value) {
for(let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
// array.js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort'
]
methodsToPatch.forEach(method => {
arrayMethods[method] = function (...args) {
const result = arrayProto[method].apply(this, args)
const ob = this.__ob__
var inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2)
default:
break;
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
}
})
# Vue 这种数据劫持的方式会不会带来额外的问题,Vue3 在这些问题上有优化么
# 为什么只对对象劫持,而要对数组进行方法重写?
obj.defin可以劫持数组,但是:
因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案
# $set和$forceupdate 都做了哪些事
# 模板编译原理
https://juejin.cn/post/6969563640416436232
# Vue的diff算法原理是什么?
Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针方式比较
- 先比较两个节点是不是相同节点
- 相同节点比较属性,复用老节点
- 先比较儿子节点,考虑老节点和新节点儿子的情况
- 优化比较:头头、尾尾、头尾、尾头
- 比对查找,进行复用
# 组件写name有啥好处?
- 增加name属性,会在components属性中增加组件本身,实现组件的递归调用。
- 可以表示组件的具体名称,方便调试和查找对应的组件。
# keepalive
# 谈谈Vue的性能优化有哪些?
- 数据层级不要过深,合理的设置响应式数据
- 使用数据时,缓存值的结果,不频繁取值
- 合理设置key
- v-show(频繁切换性能高)和v-if的合理使用
- 控制组件的粒度 -> Vue采用组件级别更新
- 采用函数式组件 -> 函数式组价开销低
- 采用异步组件 -> 借助webpack的分包策略
- 使用keep-alive来缓存组件
- 虚拟滚动、时间分片等策略
- 打包优化
# Vue 的单文件开发模式,这个解析 vue-loader 是如何实现的
# vue scoped 是怎么实现的,dom 上的哈希是如何和 style 中的哈希对应起来的,又是如何保证每次生成的哈希不变的
# Vue 调用 render 函数的时机是在什么时机被触发的,后续状态变更导致 render 又是谁触发的
# vue 是如何保证父组件重新渲染不导致子级重新渲染的
# Vue 从修改属性到渲染到页面上都经历了什么
# 页面第一次加载会触发哪些Vue的生命周期
# filter原理
# vue2和vue3的区别 (vue3快在哪)
- diff算法增加patchFlag静态标识,只对比有静态标识的dom元素
- 事件增加缓存
- 很多文本节点提升 只定义一次,渲染时不需要再次定义,vue2每次都需要重新定义
- ssr渲染 以字符串方式渲染
- proxy替换了之前的defineProterty
- vite
- ts
- tree shaking
依赖收集和派发更新都是如何做的
# 路由
https://zhuanlan.zhihu.com/p/37730038
# 来由
最开始是后端形成的概念,解析url的路径,形成路由。
后来随着ajax的流行,前端也开始流行单页面应用,所以也有了路由的概念。
# 实现原理
# hash
最开始通过hash的方式实现
http://www.xxx.com/#/login
hash值变化不会触发页面刷新,同时还能通过hashchange
这个事件监听到页面路径变化
function matchAndUpdate () {
// todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', matchAndUpdate)
# history
h5推出后,有了新的api:history(pushState和
replaceState)
原理跟hash类似,但没有#了。
但刷新页面时浏览器会向服务端发送请求,所以需要服务端处理,把所有路由都重定向到根目录
function matchAndUpdate () {
// todo 匹配路径 做 dom 更新操作
}
window.addEventListener('popstate', matchAndUpdate)
# vue-router
# 使用方法
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [...]
})
new Vue({
router
...
})
# install
Vue 通过 use 方法,加载VueRouter
中的 install 方法。install 完成 Vue 实例对 VueRouter 的挂载过程。下面我们来分析一下具体的执行过程:
export function install (Vue) {
// ...
// 混入 beforeCreate 钩子
Vue.mixin({
beforeCreate () {
// 在option上面存在router则代表是根组件
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 执行_router实例的 init 方法
this._router.init(this)
// 为 vue 实例定义数据劫持
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件则直接从父组件中获取
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 设置代理,当访问 this.$router 的时候,代理到 this._routerRoot._router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 设置代理,当访问 this.$route 的时候,代理到 this._routerRoot._route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册 router-view 和 router-link 组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// Vue钩子合并策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
// ...
}
// 执行 vm.$options._parentVnode.data.registerRouteInstance 渲染 router-view 组件
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
总结一下,就是定义了$router
、$route
以及注册了router-view 和 router-link 组件
# init
初始化mode
hash、history、abstract
如果不支持history将切换到hash
非浏览器用abstract
然后通过history
来确定不同路由的切换动作动作history.transitionTo
。最后通过history.listen
来注册路由变化的响应回调。
# 动态加载路由
系统权限的实现和控制
页面
按钮
# vuex
Vuex是专门为vue提供的全局状态管理系统,用于多个组件中的数据共享、数据缓存。
问题:无法持久化。有持久化方案
# vue中使用了哪些设计模式?
- 单例模式:new多次,只有一个实例
- 工场模式:传入参数就可以创建实例(虚拟节点的创建)
- 发布订阅模式:eventBus
- 观察者模式:watch和dep
- 代理模式:_data属性、proxy、防抖、节流
- 中介者模式:vuex
- 策略模式
- 外观模式
# 实现 ob 和 watch 方法,希望当方法传入 watch 函数时会执行一次,之后每次修改 data 上的属性时,会触发对应的 console
const data = ob({ count: 0, foo: 'test' });
watch(() => {
console.log('watch-count', data.count);
});
watch(() => {
console.log('watch-foo', data.foo);
});
data.count += 1;
console.log('showcount', data.count);
delete data.count;
data.foo = 'test2';
# 浏览器
# 重绘和回流
# 是什么
重绘:改变了尺寸等影响布局的css时就会重绘 回流:不影响布局的,例如颜色等 回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流 当页面布局和几何属性改变时就需要回流 比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变
# 如何优化
浏览器 浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。 自己 比如改变样式的时候,不去改变他们每个的样式,而是直接改变className 就要用到cssText 但是要注意有一个问题,会把原有的cssText清掉,比如原来的style中有’display:none;’,那么执行完上面的JS后,display就被删掉了。 为了解决这个问题,可以采用cssText累加的方法,但是IE不支持累加,前面添一个分号可以解决。 还有添加节点的时候比如要添加一个div里面有三个子元素p,如果添加div再在里面添加三次p,这样就触发很多次回流和重绘,我们可以用cloneNode(true or false) 来避免,一次把要添加的都克隆好再appened就好了
# 如果有必须回流的应该怎么办
# chrome 浏览器最多同时加载多少个资源,那如果想同时加载更多资源应该怎么办
# 介绍一下浏览器的合成层
# 浏览器请求头和响应头都能记起哪些,都是做什么的
# 协商缓存与强缓存
# Access-Control-Allow-Origin用 * 和指定域名的区别是什么
# 跨域是否允许携带cookie,如果希望携带cookie需要如何做,如果a.com是我的域名,向b.com发请求,带的是哪个域名的cookie
# 请求头的host,origin,refer的区别是什么
# 在什么场景下会发起options请求
# 离线存储是如何做的
# 内核都有哪些
# chrome浏览器开启一个页签时开启了多少个进程,对应开启了哪些线程
# 异步加载js的方式都有哪些
# 加载css和js时会阻塞dom渲染么
# cookie都有哪些属性
# samesite作用是什么
# 小程序
# 跨端
# h5 项目依赖的安卓和苹果的 webview 的内核分别都是什么
# 移动端有没有遇到过滑动穿透的问题
# 移动端浏览器兼容问题
# 一个PC页面,如果需要适配手机屏幕,都有哪些需要注意的,可能需要解决哪些问题,需要如何去测试
# 安全
# http
# 一个简单请求的header会有什么字段
Accept
:浏览器支持的MIME类型Accept-Encoding:gzip, deflate
:浏览器支持的压缩编码是gzip和deflateAccept-Language:zh-cn,zh;q=0.5
:浏览器支持的语言是简体中文和中文,优先支持前者Connection:keep-alive
:客户端与服务端的连接类型为持久连接Content-length
:内容长度Content-type
:内容格式Host
:域名Origin
:访问来源地址(protocal + host)Referer
:访问来源地址(完整)
# Websocket
# content-type的类型有哪些
- application/json
- multipart/form-data
- raw
- binary
- plain/text
# http请求的方法有哪些
- get
- post
- head
- put
- delete
- connect
- trace
- options
get和post区别 发起post请求的时候,服务端是怎么解析你的body的(content-type),常见的content-type都有哪些(我说了postman里的那几个),发文件是怎么解析的(FormData),如果多个文件,文件之间是如何分割的(boundary)
# 加密算法都了解哪些,都有什么应用场景
- TCP 三次握手 http1.0,1.1,2 都有哪些区别
- https,为什么 https 可以防中间人攻击
# 1.1有什么不好的地方么,队头阻塞是什么引起的
# 2.0有没有完全解决了队头阻塞问题
# get和post有什么区别
# 幂等与非幂等的区别
# get请求是否可以传图片
# https
# http2 的多路复用是什么原理
# 数字签名
输入url回车之后到渲染页面都发生了什么,哪些地方可以优化
# 设计模式
单例、工厂、适配器、装饰器、发布订阅、代理
- 介绍一下单例模式和它在前端的应用
# 工程化
# webpack
# loader
# 常用的
file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader
:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
svgo-loader
:压缩 SVG
svg-sprite-loader
: 将svg在html中引用,可通过名字就去使用svg
image-loader
:加载并且压缩图片文件
babel-loader
:把 ES6 转换成 ES5
less-loader
:将SCSS/SASS代码转换成CSS
css-loader
:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader
:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
eslint-loader
:通过 ESLint 检查 JavaScript 代码
vue-loader
:加载 Vue.js 单文件组件
# 编写loader的思路
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的API (opens new window) 可以去官网查阅
- Loader 运行在 Node.js 中,我们可以调用任意 Node.js 自带的 API 或者安装第三方模块进行调用
- Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串,当某些场景下 Loader 处理二进制文件时,需要通过 exports.raw = true 告诉 Webpack 该 Loader 是否需要二进制数据
- 尽可能的异步化 Loader,如果计算量很小,同步也可以
- Loader 是无状态的,我们不应该在 Loader 中保留状态
- 使用 loader-utils 和 schema-utils 为我们提供的实用工具
- 加载本地 Loader 方法
- Npm link
- ResolveLoader
# plugin
# 常用的
webpackbar
: 显示进度条
uglifyjs-webpack-plugin
:不支持 ES6 压缩 (Webpack4 以前)
terser-webpack-plugin
: 支持压缩 ES6 (Webpack4),在5中成为默认压缩插件,也支持了多进程
webpack-parallel-uglify-plugin
: 多进程执行代码压缩,提升构建速度
mini-css-extract-plugin
: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
clean-webpack-plugin
: 目录清理,webpack5自带了
webpack-bundle-analyzer
: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)
# 提高效率的插件
webpack-merge
:提取公共配置,减少重复配置代码
size-plugin
:监控每个chunk体积变化,尽早发现问题
# 编写Plugin的思路
webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
Plugin的API (opens new window) 可以去官网查阅
- compiler 暴露了和 Webpack 整个生命周期相关的钩子
- compilation 暴露了与模块和依赖有关的粒度更小的事件钩子
- 插件需要在其原型上绑定apply方法,才能访问 compiler 实例
- 传给每个插件的 compiler 和 compilation对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件
- 找出合适的事件点去完成想要的功能
- emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(emit 事件是修改 Webpack 输出资源的最后时机)
- watch-run 当依赖的文件发生变化时会触发
- 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
# Loader和Plugin的区别
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader
在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
# Webpack构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数
:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数开始编译
:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口
:根据配置中的 entry 找出所有的入口文件编译模块
:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译
:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源
:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成
:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
简单说
- 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
- 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中
# source map是什么?生产环境怎么用?
模式 | 解释 |
---|---|
eval | 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL . |
source-map | 生成一个SourceMap文件. |
hidden-source-map | 和 source-map 一样,但不会在 bundle 末尾追加注释. |
inline-source-map | 生成一个 DataUrl 形式的 SourceMap 文件. |
eval-source-map | 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap. |
cheap-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的 sourcemap) |
cheap-module-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。 |
source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
hidden-source-map
:借助第三方错误监控平台 Sentry 使用nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高sourcemap
:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)
注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle 体积大小,并降低整体性能。
# 模块打包原理
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
# 文件监听原理
在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:
- 启动 webpack 命令时,带上 --watch 参数
- 在配置 webpack.config.js 中设置 watch:true
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
# 热更新原理
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
细节请参考Webpack HMR 原理解析 (opens new window)
# 文件指纹是什么?怎么用?
文件指纹是打包后输出的文件名的后缀。
Hash
:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改Chunkhash
:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhashContenthash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
# JS的文件指纹设置
设置 output 的 filename,用 chunkhash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
}
}
# CSS的文件指纹设置
设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
]
}
# 图片的文件指纹设置
设置file-loader的name,使用hash。
占位符名称及含义
- ext 资源后缀名
- name 文件名称
- path 文件的相对路径
- folder 文件所在的文件夹
- contenthash 文件的内容hash,默认是md5生成
- hash 文件内容的hash,默认是md5生成
- emoji 一个随机的指代文件内容的emoj
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
},
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}
# 如何保证各个loader按照预想方式工作?
可以使用 enforce
强制执行 loader
的作用顺序,pre
代表在所有正常 loader 之前执行,post
是所有 loader 之后执行。(inline 官方不推荐使用)
# 优化 Webpack 的构建速度
# 使用高版本
的 Webpack 和 Node.js
# 多进程/多实例构建
HappyPack(不维护了)、thread-loader
# 压缩代码
- 多进程并行压缩
- webpack-paralle-uglify-plugin
- uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
- terser-webpack-plugin 开启 parallel 参数
- 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
# 图片压缩
- 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
- 配置 image-webpack-loader
# 缩小打包作用域
- exclude/include (确定 loader 规则范围)
- resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
- resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
- resolve.extensions 尽可能减少后缀尝试的可能性
- noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
- IgnorePlugin (完全排除模块)
- 合理使用alias
# 提取页面公共资源
- 基础包分离:
- 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
- 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
# DLL
- 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
- HashedModuleIdsPlugin 可以解决模块数字id问题
# 充分利用缓存提升二次构建速度
- babel-loader 开启缓存
- terser-webpack-plugin 开启缓存
- 使用 cache-loader 或者 hard-source-webpack-plugin
# Tree shaking
- 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
- 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
- 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
- purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
# Scope hoisting
- 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
- 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
# 动态Polyfill
- 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
# webpack与gulp区别
gulp是自动化构建工作流。是对文件内容进行转换。
webpack实现了对esModule新特性的打包,还可以配置打包模块的体积和数量;
webpack可以对所有资源进行打包,不管是图片、文件、还是css、js。
webpack有两大功能:loader和plugin。更灵活的编译、打包项目。
webpack还有很多高级功能:shake tree、热插拔。
# 异步加载和分包的原理是什么
# 公用的代码如何做提取,如何判断一个资源是否应该被提取
# 打 polyfill 都有哪几种方式
# Rollup和webpack打包结果有什么异同
# vite
- webpack使用ployfill注册依赖
- vite使用了ESM注册依赖
- vite打包体积更小 vite更快 vite配置更简单
# webpack 迁移 Vite 有遇到什么问题,snowpack 有了解过么,它和 vite 有什么区别
# babel
# preset和plugin谁的优先级高
# babel.config.js 和.babelrc 有什么区别,应该在什么场景使用,同时使用的话会出现什么现象
# 原理
大多数JavaScript Parser遵循 estree
规范,Babel 最初基于 acorn
项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:
- 解析:将代码转换成 AST
- 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
- 语法分析:分析token流(上面生成的数组)并生成 AST
- 转换:访问 AST 的节点进行变换操作生产新的 AST
- Taro (opens new window)就是利用 babel 完成的小程序语法转换
- 生成:以新的 AST 为基础生成代码
想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目 the-super-tiny-compiler (opens new window)
# 项目管理
# 代码规范
# 项目代码规范是如何做的,如何避免有人本地跳过代码规范
# git commit的有限制么
# eslint和prettier的冲突是如何解决的
# CI和CD
区别,除了gitlab的CI/CD之外还接触过哪些
# docker有了解么,有实际用过么
# 新技术
# serverless有哪些了解
# wsam
# 算法
# 数组
# 4. 寻找两个正序数组的中位数 (opens new window)
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
# 合并数组找中间值
时间O((m+n)log (m+n)) ,空间O(m+n)
var findMedianSortedArrays = function(nums1, nums2) {
var arr = nums1.concat(nums2)
arr = arr.sort((a, b) => a - b)
var length = arr.length
if (length % 2 === 0) {
var rightIndex = length / 2
var right = arr[rightIndex]
var left = arr[rightIndex - 1]
return (right + left) / 2
} else {
return arr[Math.floor(length / 2)]
}
};
# 双指针
时间O(log (m+n)) ,空间O(1)
var findMedianSortedArrays = function(nums1, nums2) {
let n1 = nums1.length;
let n2 = nums2.length;
// 两个数组总长度
let len = n1 + n2;
// 保存当前移动的指针的值(在nums1或nums2移动),和上一个值
let preValue = -1;
let curValue = -1;
// 两个指针分别在nums1和nums2上移动
let point1 = 0;
let point2 = 0;
// 需要遍历len/2次,当len是奇数时,最后取curValue的值,是偶数时,最后取(preValue + curValue)/2的值
for (let i = 0; i <= Math.floor(len/2); i++) {
preValue = curValue;
// 需要在nums1上移动point1指针
// 1没走完且(2走完了或者1小于2时),走1,否则走2
if (point1 < n1 && (point2 >= n2 || nums1[point1] < nums2[point2])) {
curValue = nums1[point1];
point1++;
} else {
curValue = nums2[point2];
point2++;
}
}
return len % 2 === 0
? (preValue + curValue) / 2
: curValue
};
# 11. 盛最多水的容器 (opens new window)
示例 1:
输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
# 暴力
两层for循环 找出所有的可能,记录最大值
时间O(n^2),空间O(1)
# 双指针
时间O(log(n)),空间O(1)
var maxArea = function(a) {
let max = 0
let l = 0
let r = a.length - 1
while(l < r) {
const minHeight = a[l] <= a[r] ? a[l++] : a[r--]
let area = minHeight * (r - l + 1)
max = Math.max(area, max)
}
return max
};
每次都移动自己最差的一边,虽然可能变得更差,但是总比不动(或者减小)强,动最差的部分可能找到更好的结果,但是动另一边总会更差或者不变
# 46. 全排列 (opens new window)
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
# 递归
用个数组记录,每次都循环整个数组,不存在该元素就推入数组
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
const res = []
// 做个缓存
const used = {};
const backTrack = (path) => {
if (path.length === nums.length) {
res.push(path)
return
}
nums.forEach(item => {
// if (path.includes(item)) return
if (used[item]) return;
used[item] = true
backTrack(path.concat(item))
used[item] = false
})
}
backTrack([])
return res
};
# 47. 全排列 II (opens new window)
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
# 递归
先排序,循环,有推入当前索引的或者等于前一个的就跳过
与46题的区别就是用位置去判断而不是用值去判断
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function(nums) {
const ans = [];
const vis = new Array(nums.length).fill(false);
const backtrack = (idx, perm) => {
if (idx === nums.length) {
ans.push(perm.slice());
return;
}
for (let i = 0; i < nums.length; ++i) {
if (vis[i] || (i > 0 && nums[i] === nums[i - 1] && !vis[i - 1])) {
continue;
}
perm.push(nums[i]);
vis[i] = true;
backtrack(idx + 1, perm);
vis[i] = false;
perm.pop();
}
}
nums.sort((x, y) => x - y);
backtrack(0, []);
return ans;
};
# 48. 旋转图像 (opens new window)
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]
# 两次for循环
时间O(n^2) 空间 O(1)
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var rotate = function(matrix) {
let temp;
for (let x = 0, y = matrix[0].length - 1; x < y; x++, y--) {
for (let s = x, e = y; s < y; s++, e--) {
temp = matrix[x][s];
matrix[x][s] = matrix[e][x];
matrix[e][x] = matrix[y][e];
matrix[y][e] = matrix[s][y];
matrix[s][y] = temp;
};
};
};
# 翻转
var rotate = function(matrix) {
const n = matrix.length;
// 水平翻转
for (let i = 0; i < Math.floor(n / 2); i++) {
for (let j = 0; j < n; j++) {
[matrix[i][j], matrix[n - i - 1][j]] = [matrix[n - i - 1][j], matrix[i][j]];
}
}
// 主对角线翻转
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
};
# 53. 最大子数组和 (opens new window)
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
# 动态规划
如果sum大于0,则说明对结果有益,加上当前的。
否则就等于当前的值。
同时还要比较返回的值
时间O(N) 空间O(1)
/**
* @param {number[]} nums
* @return {number}
*/
var maxSubArray = function(nums) {
let ans = nums[0];
let sum = 0;
for(const num of nums) {
// if(sum > 0) { 可以写成这样
if(sum + num > num ){
sum += num;
} else {
sum = num;
}
ans = Math.max(ans, sum);
}
return ans;
};
# 54. 螺旋矩阵 (opens new window)
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]
# 找规律
时间O(N) 空间O(1)
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function (arr) {
let res = [];
if (!arr.length) return res;
let l = 0, r = arr[0].length - 1;
let t = 0, b = arr.length - 1;
while (true) {
for (let i = l; i <= r; i++) res.push(arr[l][i]);
if (++t > b) return res;
for (let j = t; j <= b; j++) res.push(arr[j][r]);
if (--r < l) return res;
for (let m = r; m >= l; m--) res.push(arr[b][m]);
if (--b < t) return res;
for (let n = b; n >= t; n--) res.push(arr[n][l]);
if (++l > r) return res;
}
};
# 56. 合并区间 (opens new window)
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
- 从小到大排序
- 用个res记录所有区间
- 当下一个的左边界在当前区间内时,找出两个区间的最大右边界,修改当前区间
- 否则就是一个新的区间,推入
/**
* @param {number[][]} intervals
* @return {number[][]}
*/
var merge = function(intervals) {
intervals.sort((a,b) => a[0]-b[0])
let res = {
val:[intervals[0]],
cur:0
}
for(let i=1;i<intervals.length;i++) {
const {val, cur} = res
if(intervals[i][0]>=val[cur][0] && intervals[i][0]<=val[cur][1]) {
val[cur][1] = Math.max(intervals[i][1], val[cur][1])
}else {
val.push(intervals[i])
res.cur++
}
}
return res.val
};
# 63. 不同路径 II (opens new window)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] 输出:2 解释:3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
# 动态规划
用个同样的数组记录
时间O(N^2),空间O(N^2)
var uniquePathsWithObstacles = function(obstacleGrid) {
const m = obstacleGrid.length;
const n = obstacleGrid[0].length;
const arr = new Array(m).fill(0);
for (let i = 0; i < m; i++) {
arr[i] = new Array(n).fill(0)
}
if (obstacleGrid[0][0] !== 1) {
arr[0][0] = 1
}
for (let j = 1; j < n; j++) {
arr[0][j] = obstacleGrid[0][j] === 1 ? 0 : arr[0][j - 1]
}
for (let i = 1; i < m; i++) {
arr[i][0] = obstacleGrid[i][0] === 1 ? 0 : arr[i - 1][0]
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
arr[i][j] = obstacleGrid[i][j] === 1 ? 0 : arr[i -1][j] + arr[i][j - 1]
}
}
return arr[m -1][n - 1]
}
# 动态规划-降维
时间O(N*M),空间O(N)
/**
* @param {number[][]} obstacleGrid
* @return {number}
*/
var uniquePathsWithObstacles = function(obstacleGrid) {
const m = obstacleGrid.length;
const n = obstacleGrid[0].length;
const arr = new Array(n).fill(0);
arr[0] = 1
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (obstacleGrid[i][j] === 1) {
arr[j] = 0;
} else if (j > 0) {
arr[j] += arr[j - 1]
}
}
}
return arr[n - 1]
};
// [ 1, 1, 1 ]
// [ 1, 0, 1 ]
// [ 1, 1, 2 ]
// 等于左边的加上上面的
# 64. 最小路径和 (opens new window)
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
# 动态规划
时间O(N*M),空间O(1)
/**
* @param {number[][]} grid
* @return {number}
*/
var minPathSum = function(grid) {
let row = grid.length, col = grid[0].length
for(let i = 0; i < row; i++) {
for(let j = 0; j < col; j++) {
if (i === 0 && j !== 0) {
grid[i][j] += grid[i][j - 1]
} else if (j === 0 && i !== 0) {
grid[i][j] += grid[i - 1][j]
} else if (i !== 0 && j !== 0) {
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1])
}
}
}
return grid[row - 1][col - 1]
};
# 66. 加一 (opens new window)
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入:digits = [1,2,3] 输出:[1,2,4] 解释:输入数组表示数字 123。
# 判断进位
从最后一位开始加1,先处理9的情况,如果是9,加1的位置要一直前移,直到最开始或者某个不为9的位。 找到最后一个加1不影响其他位的位置就停止。
时间最长O(N),空间O(1)
/**
* @param {number[]} digits
* @return {number[]}
*/
var plusOne = function(digits) {
let { length } = digits;
var needAdd = false;
while(length > 0) {
if (digits[--length] === 9) {
digits[length] = 0
needAdd = true
} else {
digits[length] = digits[length] + 1;
needAdd = false
break
}
}
if (needAdd) {
digits.unshift(1)
}
return digits
};
# 74. 搜索二维矩阵 (opens new window)
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。 每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 输出:true
# 二分法
直接找总数的中值,再转成二维坐标去二分
时间O(log(N)) 空间O(1)
var searchMatrix = function(matrix, target) {
const m = matrix.length, n = matrix[0].length;
let low = 0, high = m * n - 1;
while (low <= high) {
const mid = Math.floor((high - low) / 2) + low;
const x = matrix[Math.floor(mid / n)][mid % n];//一维坐标转换成二维坐标
if (x < target) {
low = mid + 1;
} else if (x > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
}
# 75. 颜色分类 (opens new window)
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库的sort函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
# 计数
记录各颜色数量然后重新赋值
时间O(N) 空间O(1)
var sortColors = function(nums) {
let arr = [0,0,0]
for (let i = 0; i < nums.length; i++) {
const item = nums[i]
arr[item] += 1
}
for (let i = 0, j = 0; i < arr.length; i++) {
while (arr[i]--) {
nums[j] = i
j++
}
}
}
# 双指针
a是0的位置指针 b是2的位置指针
var sortColors = function(nums) {
let a = 0, b = nums.length - 1
for (let c = 0; c <= b; c++) {
if (nums[c] === 0) {
[nums[c], nums[a]] = [nums[a], nums[c]]
a++
}
if (nums[c] === 2) {
[nums[c], nums[b]] = [nums[b], nums[c]]
b--
c--
}
}
}
# 78. 子集 (opens new window)
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
# 回溯
1.用递归模拟出所有情况 2.保证接的数字都是后面的数字 3.收集所有到达递归终点的情况,并返回
时间复杂度:O(2^n),因为每个元素都有两种可能(存在或不存在) 空间复杂度:O(n)
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function(nums) {
const res = []
const backTrack = (path, length, start) => {
if (path.length === length) {
res.push(path)
return
}
for (let i = start; i < nums.length; i++) {
backTrack(path.concat(nums[i]), length, i + 1)
}
}
for (let i = 0; i <= nums.length; i++) {
backTrack([], i, 0)
}
return res
};
# 120. 三角形最小路径和 (opens new window)
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
倒着加上来,00位置就是最小值
/**
* @param {number[][]} triangle
* @return {number}
*/
var minimumTotal = function(triangle) {
let n = triangle.length
if(!n) return 0
let dp = Array.apply(null, Array(n)).map((t, i) => Array(i + 1).fill(0))
dp[n - 1] = triangle[n - 1]
for(let i = n - 2; i >= 0; i--){
for(let j = 0; j < i + 1; j++){
// 用下一行小的加上当前位置,倒过来求和
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]
}
}
return dp[0][0]
};
# 122. 买卖股票的最佳时机 II (opens new window)
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。 返回 你能获得的 最大 利润 。
示例 1:
输入: prices = [7,1,5,3,6,4] 输出: 7 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
# 贪心
一旦有赚就卖掉
/**
* @param {number[]} prices
* @return {number}
*/
var maxProfit = function(prices) {
let profit = 0
for (let i = 1;i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1]
}
}
return profit
};
# 136. 只出现一次的数字 (opens new window)
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1] 输出: 1
# 额外空间
用个对象的键值记录,重复了就删掉,剩下的那个键就是1个的
# 位异或
有相同的值去异或就等于没异或
所以可以用来去重
时间O(N) 空间 O(1)
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
let res = 0
for (let i = 0 ; i< nums.length; i++) {
res ^= nums[i]
}
return res
};
# 137. 只出现一次的数字 II (opens new window)
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
示例 1:
输入:nums = [2,2,3,2] 输出:3
# map记录
/**
* @param {number[]} nums
* @return {number}
*/
var singleNumber = function(nums) {
var map = new Map()
nums.forEach(item => {
if (map.has(item)) {
map.set(item, map.get(item) + 1)
} else {
map.set(item,1)
}
})
let res
map.forEach((item, k) => {
if (map.get(k) === 1) {
res = k
}
})
return res
};
# 位运算
注意到每个数字的最大范围是 -231 <= nums[i] <= 231 - 1,这很明显提示了要使用位运算来解。在 JS 中数字都是 IEEE-754 标准的 Double 类型(64 位)。但是位运算结果只有 32 位。
由于只有一个元素出现了一下,其他都是三次,那么假设遍历一遍每一个数字的同一位,统计一下总共有多少个数字这一位为数字 1。如果得到的结果不能被三整除,说明导致不能被整除的 1 肯定来自于只出现一次的那个数。
使用或运算把结果“累加”起来即可。
var singleNumber = function(nums) {
let ans = 0
for (let i = 0; i < 32; i ++) {
// 统计多少个数字,第 i 位为 1。
let num = nums.reduce((cur, next) => {
return cur + ((next & (1 << i)) ? 1 : 0)
}, 0)
// 如果不能被三整除,说明这一位在只出现一次的那个数上是 1。
if (num % 3 !== 0) {
ans |= (1 << i)
}
}
return ans
};
# 153. 寻找旋转排序数组中的最小值 (opens new window)
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2] 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7] 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
# 暴力
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
// 1.暴力
for (let i = 0; i < nums.length - 1; i++) {
if (nums[i] > nums[i + 1]) {
return nums[i+1]
}
}
return nums[0]
};
# 二分
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
let mid = left + Math.floor((right - left) / 2) + 1;
if (nums[left] < nums[mid]) {
left = mid;
} else if (nums[left] > nums[mid]) {
right = mid - 1;
}
}
return nums[(right + 1) % nums.length];
};
# 154. 寻找旋转排序数组中的最小值 II (opens new window)
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4] 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7] 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5] 输出:1
# 二分
/**
* @param {number[]} nums
* @return {number}
*/
var findMin = function(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else {
right--
}
}
return nums[left];
};
# 169. 多数元素 (opens new window)
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:[3,2,3] 输出:3
# map记录
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let map = new Map()
const length = nums.length / 2
if (length < 1) {
return nums[0]
}
for (let i = 0; i < nums.length; i++) {
const item = nums[i]
if (map.has(item)) {
const num = map.get(item)+1
if (num > length) {
return item
}
map.set(item, num)
} else {
map.set(item, 1)
}
}
};
# 投票法
相同加1,不同减1,最后记录的就是多的
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function(nums) {
let count = 0;
let candidate;
for (let a of nums) {
if (count == 0) candidate = a;
count += candidate == a ? 1 : -1;
}
return candidate;
};
# 189. 轮转数组 (opens new window)
给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]
# api
/**
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function(nums, k) {
const length = nums.length
if (k > length) {
k = k % length
}
nums.unshift(...nums.splice(length - k, length))
};
# 翻转三次
1234567
765 4321
567 4321
567 1234
let reverse = function(nums, start, end){
while(start < end){
[nums[start++], nums[end--]] = [nums[end], nums[start]];
}
}
let rotate = function(nums, k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
return nums;
};
# 198. 打家劫舍 (opens new window)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
# 动态规划
这一次的等于上一次的或者上上次的加上这次的中间的最大值
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
const {length} = nums;
if (length === 1) {
return nums[0]
} else if (length === 2) {
return Math.max(nums[0], nums[1])
}
nums[1] = Math.max(nums[0], nums[1]);
for (let i = 2; i < length; i++) {
nums[i] = Math.max(nums[i-1], nums[i-2] + nums[i])
}
return nums[length - 1]
};
# 215. 数组中的第K个最大元素 (opens new window)
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2 输出: 5
# 最小堆
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
class MinHeap {
constructor() {
this.heap = [];
}
// 交换节点位置
swap(i1, i2) {
[this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]];
}
// 获得父节点
getParentIndex(i) {
return (i - 1) >> 1;
}
// 获得左节点
getleftIndex(i) {
return 2 * i + 1;
}
// 获得右节点
getrightIndex(i) {
return 2 * i + 2;
}
// 上移
shiftUp(index) {
if (index === 0) return;
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index);
this.shiftUp(parentIndex);
}
}
// 下移
shiftDown(index) {
const leftIndex = this.getleftIndex(index);
const rightIndex = this.getrightIndex(index);
if (this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index);
this.shiftDown(leftIndex);
}
if (this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index);
this.shiftDown(rightIndex);
}
}
// 插入
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length - 1);
}
// 删除堆顶
pop() {
// pop()方法删除数组最后一个元素并返回,赋值给堆顶
this.heap[0] = this.heap.pop();
// 对堆顶重新排序
this.shiftDown(0);
}
// 获取堆顶
peek() {
return this.heap[0];
}
// 获取堆的大小
size() {
return this.heap.length;
}
}
const findKthLargest = (nums, k) => {
const minHeap = new MinHeap();
nums.forEach(n => {
// 将数组元素依次插入堆中
minHeap.insert(n);
// 如果堆大小超过k, 开始裁员, 将堆顶(最小) 的去掉
if (minHeap.size() > k) {
minHeap.pop();
}
})
// 返回堆顶,此时就是第k大的元素
return minHeap.peek();
};
# 219. 存在重复元素 II (opens new window)
给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,1], k = 3 输出:true
# 直接循环找
不用前后都找,前面的在之前的元素已经找过了
/**
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
var containsNearbyDuplicate = function(nums, k) {
for (let i = 0; i < nums.length; i++) {
const item = nums[i]
for (let j = i + 1; j < i + k + 1; j++) {
if (item === nums[j]) return true
}
}
return false
};
# Set记录
维护一个哈希表,里面始终最多包含 k 个元素,当出现重复值时则说明在 k 距离内存在重复元素 每次遍历一个元素则将其加入哈希表中,如果哈希表的大小大于 k,则移除最前面的数字 时间复杂度:O(n)O(n),nn 为数组长度
/**
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
var containsNearbyDuplicate = function(nums, k) {
const set = new Set();
for(let i = 0; i < nums.length; i++) {
if(set.has(nums[i])) {
return true;
}
set.add(nums[i]);
if(set.size > k) {
set.delete(nums[i - k]);
}
}
return false;
};
# 228. 汇总区间 (opens new window)
给定一个 无重复元素 的 有序 整数数组 nums 。
返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums 的数字 x 。
列表中的每个区间范围 [a,b] 应该按如下格式输出:
"a->b" ,如果 a != b "a" ,如果 a == b
示例 1:
输入:nums = [0,1,2,4,5,7] 输出:["0->2","4->5","7"] 解释:区间范围是: [0,2] --> "0->2" [4,5] --> "4->5" [7,7] --> "7"
# 双指针
/**
* @param {number[]} nums
* @return {string[]}
*/
var summaryRanges = function(nums) {
const res = []
let left = 0, right = 0
for (let i = 0 ; i < nums.length; i++) {
const next = i + 1;
if ((nums[next] - nums[i]) !== 1) {
if (right === left) {
res.push(nums[left] + '')
} else {
res.push(`${nums[left]}->${nums[right]}`)
}
left = next;
right = next;
} else {
right = next;
}
}
return res
};
# 239. 滑动窗口最大值 (opens new window)
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
# 优先队列
var MonotonicQueue = function () {
let queue = [];
// 在队尾添加元素 n
this.push = function (n) {
// 将前面小于自己的元素都删除
while (queue.length && queue[queue.length - 1] < n) {
queue.pop();
}
queue.push(n);
};
// 返回当前队列中的最大值
this.max = function () {
// 队头的元素肯定是最大的
return queue[0];
};
// 队头元素如果是 n,删除它
this.pop = function (n) {
if (n == queue[0]) {
queue.shift();
}
};
};
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
let window = new MonotonicQueue();
let res = [];
for (let i = 0; i < nums.length; i++) {
if (i < k - 1) {
//先把窗口的前 k - 1 填满
window.push(nums[i]);
} else {
// 窗口开始向前滑动
// 移入新元素
window.push(nums[i]);
// 将当前窗口中的最大元素记入结果
res.push(window.max());
// 移出最后的元素
window.pop(nums[i - k + 1]);
}
}
return res;
};
# 268. 丢失的数字 (opens new window)
给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。
示例 1:
输入:nums = [3,0,1] 输出:2 解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
# 求和
var missingNumber = function(nums) {
const {length} = nums
const all = (length + 1) * length / 2
let res = nums.reduce((prev, next) => {
return prev + next
}, 0)
return all - res
};
# 异或
var missingNumber = function(nums) {
const {length} = nums
let res = 0
for (let i = 0; i < length; i++) {
res ^= i ^ nums[i]
}
return res ^ length
};
# 283. 移动零 (opens new window)
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
# 原地删除
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let zeroNum = 0
const {length} = nums
for (let i = 0; i < length; i++) {
if (zeroNum === length - i) return
const num = nums[i]
if (num === 0) {
zeroNum++
nums.splice(i, 1)
nums.push(0)
i--
}
}
};
# 快慢指针
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function (nums) {
let fast = 0,
slow = 0;
while (fast < nums.length) {
if (nums[fast] != 0) {
[nums[fast], nums[slow]] = [nums[slow], nums[fast]];
slow++;
}
fast++;
}
};
# 289. 生命游戏 (opens new window)
根据 百度百科 , 生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡; 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活; 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡; 如果死细胞周围正好有三个活细胞,则该位置死细胞复活; 下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。
示例 1:
输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] 输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
# 加额外状态
/**
* @param {number[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
var gameOfLife = function(board) {
// var oldBorad = JSON.parse(JSON.stringify(board))
const xLength = board.length
const yLength = board[0].length
for (let x =0; x < xLength; x++) {
for (let y =0; y < yLength; y++) {
board[x][y] = getPositionNeighbour(board, x, y)
}
}
for (let x =0; x < xLength; x++) {
for (let y =0; y < yLength; y++) {
board[x][y] = board[x][y] % 2
}
}
};
function getPositionNeighbour(oldBorad, x, y) {
let liveNum = 0;
for (let i =-1; i < 2; i++) {
for (let j =-1; j < 2; j++) {
if (!(i === 0 && j === 0 )) {
if (oldBorad[x+i] !== undefined && oldBorad[x+i][y+j] !== undefined) {
const item = oldBorad[x+i][y+j]
if (item === 1 || item === 2) {
liveNum++
}
}
}
}
}
// 2,代表 生->死,加个状态 3 表示从 死>生
var item = oldBorad[x][y]
if (liveNum < 2) {
return item === 1 ? 2 : 0
} else if (liveNum === 2) {
return item === 1 ? 1 : 0
} else if (liveNum === 3) {
return item === 1 ? 1 : 3
} else if (liveNum > 3) {
return item === 1 ? 2 : 0
}
}
# 300. 最长递增子序列 (opens new window)
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function (nums) {
const select = (a, b) => {
if (a > b) return a;
else return b;
};
let n = nums.length;
if (n < 2) return n;
let dp = new Array(n).fill(1),
max = 0;
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
// 记录每个位置前面比他小的个数
// 如果dp[j]的最长数比当前i的大,则说明是连续的
dp[i] = select(dp[i], dp[j] + 1);
}
}
max = select(max, dp[i]);
}
return max;
};
# 322. 零钱兑换 (opens new window)
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1
/**
* @param {number[]} coins
* @param {number} amount
* @return {number}
*/
var coinChange = function(coins, amount) {
let dp = new Array( amount + 1 ).fill( Infinity );
dp[0] = 0;
for (let i = 1;i <= amount; i++) {
for (let coin of coins) {
if (i - coin >= 0) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1)
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount]
};
# 347. 前 K 个高频元素 (opens new window)
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
class MinHeap {
constructor () {
this.heap = []
}
swap (i1, i2) {
[this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]]
}
getLeftIndex (i) {
return i * 2 + 1
}
getRightIndex (i) {
return i * 2 + 2
}
getParentIndex(i) {
return (i - 1) >> 1
}
shiftUp (index) {
if (index === 0) return
const parentIndex = this.getParentIndex(index)
if (this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value) {
this.swap(parentIndex, index)
this.shiftUp(parentIndex)
}
}
shiftDown (index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if (this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if (this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
insert(value) {
this.heap.push(value)
this.shiftUp(this.heap.length - 1)
}
pop() {
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
const map = new Map()
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n) + 1 : 1)
})
const h = new MinHeap()
map.forEach((value, key) => {
console.log(value, key)
h.insert({key, value})
if (h.size() > k) {
h.pop()
}
})
return h.heap.map(a => a.key)
};
# 349. 两个数组的交集 (opens new window)
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
# 去重加过滤
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
return [...new Set(nums1)].filter(item => nums2.includes(item))
};
# 350. 两个数组的交集 II (opens new window)
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2,2]
# Map
记录次数
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
const map = new Map()
nums1.forEach(item => {
if (map.has(item)) {
map.set(item, map.get(item) + 1)
} else {
map.set(item, 1)
}
})
const res = []
nums2.forEach(item => {
const itemNum = map.get(item)
if (itemNum) {
res.push(item)
map.set(item, itemNum - 1)
}
})
return res
};
# 双指针
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersect = function(nums1, nums2) {
nums1 = nums1.sort((a,b) => a - b)
nums2 = nums2.sort((a,b) => a - b)
let i = 0, j = 0, k = 0
while (i < nums1.length && j < nums2.length) {
const item1 = nums1[i]
const item2 = nums2[j]
if (item1 === item2) {
nums1[k] = item1
i++
j++
k++
} else if (item1 < item2) {
i++
} else if (item1 > item2) {
j++
}
}
return nums1.splice(0, k)
};
# 455. 分发饼干 (opens new window)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。
# 贪心
/**
* @param {number[]} g
* @param {number[]} s
* @return {number}
*/
var findContentChildren = function(g, s) {
const sortFunc = function (a, b) {
return a - b;
}
g.sort(sortFunc);
s.sort(sortFunc);
let i = 0;
s.forEach(item => {
if (item >= g[i]) {
i++
}
})
return i;
};
# 475. 供暖器 (opens new window)
冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。
在加热器的加热半径范围内的每个房屋都可以获得供暖。
现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。
说明:所有供暖器都遵循你的半径标准,加热的半径也一样。
示例 1:
输入: houses = [1,2,3], heaters = [2] 输出: 1 解释: 仅在位置2上有一个供暖器。如果我们将加热半径设为1,那么所有房屋就都能得到供暖。
找出每个位置离得最近的供暖器距离
再返回最大的值
var findRadius = function (houses, heaters) {
const minDis = (n, arr) => {
let l = 0, r = arr.length - 1;
while (l < r - 1) {
let m = ((l + r) / 2) | 0;
if (arr[m] == n) l = m, r = m;
if (arr[m] > n) r = m;
else l = m;
}
return Math.min(
Math.abs(n - arr[l]), Math.abs(n - arr[r]));
}
heaters.sort((a, b) => a - b);
return Math.max(...houses.map(v => minDis(v, heaters)));
};
# 628. 三个数的最大乘积 (opens new window)
给你一个整型数组 nums
,在数组中找出由三个数组成的最大乘积,并输出这个乘积。
示例 1:
输入:nums = [1,2,3]
输出:6
# 排序
如果数组中全是非负数,则排序后最大的三个数相乘即为最大乘积;如果全是非正数,则最大的三个数相乘同样也为最大乘积。
如果数组中有正数有负数,则最大乘积既可能是三个最大正数的乘积,也可能是两个最小负数(即绝对值最大)与最大正数的乘积。
综上,我们在给数组排序后,分别求出三个最大正数的乘积,以及两个最小负数与最大正数的乘积,二者之间的最大值即为所求答案。
var maximumProduct = function(nums) {
nums.sort((a, b) => a - b);
const n = nums.length;
return Math.max(nums[0] * nums[1] * nums[n - 1], nums[n - 1] * nums[n - 2] * nums[n - 3]);
};
# 线性扫描
找出最小的两个数和最大的三个数
最小两个负负得正再乘最大的数,与最大三个数的积取最大值
/**
* @param {number[]} nums
* @return {number}
*/
var maximumProduct = function(nums) {
let min1 = Number.MAX_VALUE, min2 = Number.MAX_VALUE
let max1 = -Number.MAX_VALUE, max2 = -Number.MAX_VALUE, max3 = -Number.MAX_VALUE
for (let i = 0; i < nums.length; i++) {
const item = nums[i]
if (item <= min1) {
min2 = min1
min1 = item
} else if (item <= min2) {
min2 = item
}
if (item >= max3) {
max1 = max2
max2 = max3
max3 = item
} else if (item >= max2) {
max1 = max2
max2 = item
} else if (item >= max1) {
max1 = item
}
}
return Math.max(min1 * min2 * max3, max1 * max2 * max3)
};
# 867. 转置矩阵 (opens new window)
给你一个二维整数数组 matrix, 返回 matrix 的 转置矩阵 。
矩阵的 转置 是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[1,4,7],[2,5,8],[3,6,9]]
# 暴力
双层循环,替换坐标
/**
* @param {number[][]} A
* @return {number[][]}
*/
var transpose = function(A) {
let res = []
const ALength = A.length
const A0Length = A[0].length
for (let i = 0; i < A0Length; i++) {
for (let j = 0; j < ALength; j++) {
if (!res[i]) res[i] = [];
res[i][j] = A[j][i]
}
}
return res
};
# map
直接返回每列做每行
/**
* @param {number[][]} A
* @return {number[][]}
*/
var transpose = function(A) {
return A[0].map((_, index) => A.map(row => row[index]))
};
# 875. 爱吃香蕉的珂珂 (opens new window)
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
示例 1:
输入: piles = [3,6,7,11], H = 8 输出: 4
# 二分
找出能吃完的最小值
/**
* @param {number[]} piles
* @param {number} h
* @return {number}
*/
var minEatingSpeed = function (piles, H) {
let left = 1;
let right = Math.max(...piles);
const canEat = (piles, speed, H) => {
let sumTime = 0;
for (let pile of piles) {
// 向上取整
sumTime += Math.ceil(pile / speed);
}
return sumTime <= H;
};
while (left < right) {
let mid = Math.floor((right + left) / 2);
if (canEat(piles, mid, H)) {
right = mid; // 如果能吃完,则最大值调整为mid
} else {
left = mid + 1; // 如果不能吃完,则最小值调整为mid + 1
}
}
return right;
};
# 881. 救生艇 (opens new window)
给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。
每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。
返回 承载所有人所需的最小船数 。
示例 1:
输入:people = [1,2], limit = 3 输出:1 解释:1 艘船载 (1, 2)
# 双指针
排序后,头尾相加,小于等于limit时,两边都去掉,不然只去掉右边大的
/**
* @param {number[]} people
* @param {number} limit
* @return {number}
*/
var numRescueBoats = function(people, limit) {
people.sort((a, b) => a - b)
let l = 0, r = people.length - 1, res = 0
while(l <= r) {
if ((people[l] + people[r]) <= limit) {
l++
}
r--
res++
}
return res
};
# 946. 验证栈序列 (opens new window)
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1] 输出:true 解释:我们可以按以下顺序执行: push(1), push(2), push(3), push(4), pop() -> 4, push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
# 挫版
/**
* @param {number[]} pushed
* @param {number[]} popped
* @return {boolean}
*/
var validateStackSequences = function(pushed, popped) {
const res = []
while (pushed.length && popped.length) {
if (pushed[0] === popped[0]) {
pushed.shift()
popped.shift()
} else if (res.length && res[res.length - 1] === popped[0]) {
res.pop()
popped.shift()
} else {
res.push(pushed.shift())
}
}
if (pushed.length) return false;
while (popped.length) {
if (res[res.length -1] === popped[0]) {
res.pop()
popped.shift()
} else {
return false
}
}
return true
};
# 栈
/**
* @param {number[]} pushed
* @param {number[]} popped
* @return {boolean}
*/
var validateStackSequences = function(pushed, popped) {
let stack = [];
let i = 0;
let j = 0;
while (i < pushed.length) {
stack.push(pushed[i]);
while(stack[stack.length - 1] === popped[j] && stack.length) {
j++;
stack.pop();
}
i++
}
return stack.length === 0;
};
# 977. 有序数组的平方 (opens new window)
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
# 暴力
平方后排序
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function(nums) {
return nums.map(item => Math.pow(item, 2)).sort((a, b) => a -b)
};
# 双指针
平方后最大的要么在左要么在右
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function(nums) {
let result = []
let start = 0
let end = nums.length - 1
for (let i = nums.length - 1; i >= 0; i--) {
let startItem = nums[start] * nums[start];
let endItem = nums[end] * nums[end];
if (startItem > endItem) {
result[i] = startItem
start++
} else {
result[i] = endItem
end--
}
}
return result
};
# 1029. 两地调度 (opens new window)
公司计划面试 2n 人。给你一个数组 costs ,其中 costs[i] = [aCosti, bCosti] 。第 i 人飞往 a 市的费用为 aCosti ,飞往 b 市的费用为 bCosti 。
返回将每个人都飞到 a 、b 中某座城市的最低费用,要求每个城市都有 n 人抵达。
示例 1:
输入:costs = [[10,20],[30,200],[400,50],[30,20]] 输出:110 解释: 第一个人去 a 市,费用为 10。 第二个人去 a 市,费用为 30。 第三个人去 b 市,费用为 50。 第四个人去 b 市,费用为 20。
最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。
假设全部去a,找出再去b花费小的n个。
然后这n个就不用去a,直接去b
var twoCitySchedCost = function(costs) {
let diffValue = [];
let sum = costs.reduce((prev, next) => {
diffValue.push(next[0] - next[1]);
return prev + next[1];
}, 0)
diffValue.sort((a, b) => a -b)
for (let i = 0; i < diffValue.length / 2; i++) {
sum += diffValue[i]
}
return sum
};
# 1550. 存在连续三个奇数的数组 (opens new window)
给你一个整数数组 arr,请你判断数组中是否存在连续三个元素都是奇数的情况:如果存在,请返回 true ;否则,返回 false 。
示例 1:
输入:arr = [2,6,4,1] 输出:false 解释:不存在连续三个元素都是奇数的情况。
# 暴力
/**
* @param {number[]} arr
* @return {boolean}
*/
var isJi = function (val) {
return val & 1 === 1
}
var threeConsecutiveOdds = function(arr) {
for (let i = 0; i <= arr.length - 3; i++) {
if (isJi(arr[i]) && isJi(arr[i + 1]) && isJi(arr[i + 2])) {
return true
}
}
return false
};
# 跳过不需要的
/**
* @param {number[]} arr
* @return {boolean}
*/
var isJi = function (val) {
return val & 1 === 1
}
var threeConsecutiveOdds = function(arr) {
for (let i = 0; i <= arr.length - 3; i++) {
if (isJi(arr[i])) {
if (isJi(arr[i + 1])) {
if (isJi(arr[i + 2])) {
return true
} {
i += 2
}
} else {
i++
}
}
}
return false
};
# 三个连续的相加是奇数
这就不好跳过重复的
# 输出数组中频率第二高的元素的下标
# DFS 找节点
function depthFirstSearchNoRecursion (target) {
const res = [];
const stack = target.slice();
// console.log(stack)
while (stack.length > 0) {
const node = stack.shift(); // 最上层节点出栈
res.push(node.id);
// 如果该节点有子节点,将子节点存入栈中,继续下一次循环
const len = node.children && node.children.length;
for (let i = len - 1; i >= 0; i--) {
stack.unshift(node.children[i]);
}
}
return res;
}
还有就是递归
# 1029. 两地调度 (opens new window)
公司计划面试 2n 人。给你一个数组 costs ,其中 costs[i] = [aCosti, bCosti] 。第 i 人飞往 a 市的费用为 aCosti ,飞往 b 市的费用为 bCosti 。 返回将每个人都飞到 a 、b 中某座城市的最低费用,要求每个城市都有 n 人抵达。
输入:costs = [[10,20],[30,200],[400,50],[30,20]] 输出:110 解释: 第一个人去 a 市,费用为 10。 第二个人去 a 市,费用为 30。 第三个人去 b 市,费用为 50。 第四个人去 b 市,费用为 20。 最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。
# 给定任意二维数组,输出所有的排列组合项。 比如 [['A','B'], ['a','b'], ['1', '2']],输出 ['Aa1','Aa2','Ab1','Ab2','Ba1','Ba2','Bb1','Bb2']
# 数组转成嵌套对象
["a","b","c","d"] => {a: {b: {c: {d: null}}}}
# 实现一个函数,传入一个数组,数组中每一项代表一个线段的起止位置,计算所有线段覆盖的长度总量,并编写测试用例
lineCoverage([
[0, 1],
[2, 3],
]); // 2
lineCoverage([
[0, 2],
[2, 3],
[3, 4],
]); // 4
lineCoverage([
[0, 2],
[1, 3],
[2, 4],
]); // 4
lineCoverage([
[0, 5],
[1, 3],
[2, 4],
]); // 5
lineCoverage([
[0, 6],
[2, 6],
[6, 7],
]); // 7
# 洗牌算法,如何验证这个洗牌算法可以把牌洗得足够乱
# 实现这个 pipe
const fn = pipe(addOne, addTwo, addThree, addFour); // 传入pipe的四个函数都是已实现的
fn(1); // 1 + 1 + 2 + 3 + 4 = 11,输出11
function pipe () {
const fnList = [...arguments]
return function(value) {
fnList.forEach(fn => {
value = fn(value)
})
console.log(value)
}
}
function addOne (value) {
return value + 1
}
function addTwo (value) {
return value + 2
}
function addThree (value) {
return value + 3
}
function addFour (value) {
return value + 4
}
# 编写一个方法,判断一个字符串是否是合法的 XML
const str1 = "<html><div>123</div></html>"; // true
const str2 = "<div><div>123</div><div></div></div>"; // true
const str2 = "<html><div>123</html></div>"; // false
# 在一个矩阵中查找一个字符串,可以上下左右移动,但是不能回头,如果能找到这个字符串返回 true
const str = "abcde";
const matrix = [
["0", "0", "0", "0", "0", "0"],
["0", "0", "a", "b", "0", "0"],
["0", "0", "0", "c", "d", "0"],
["0", "0", "0", "0", "e", "0"],
];
# 青蛙跳台阶,一次可以跳 1 阶,2 阶或者 3 阶,如果想跳上一个 N 阶共有几种跳法
# 写个二叉树遍历,深度优先广度优先
# 输入一个字符串,遇到方括号则对方括号内的字符串重复n次,n是方括号前面的数字,如果没有数字则为1次,可能存在嵌套
const test1 = "a2[b]a2[b2[c]]";
// abbabccbcc
const test2 = "2[3[c]]a2a";
// cccccca2a
const test3 = "[abc][d]3[e2]4";
// abcde2e2e24
# 二叉树
# 输入一个二叉树和两个 node,输出这两个 node 的最近公共祖先
# 杂
# 请写一个抽奖程序
已有参与抽奖的员工工号组成的数组 staffIds。 规则1:同一员工不可重复中奖。 规则2:每轮执行抽奖程序,入参是本轮要抽取的中奖人数n,将中奖人工号打印出来
# 屏幕内有一个矩形,有一条对角线,如果在矩形上点击,如何判断点击的位置是在对角线上方,还是下方,还是点到了对角线上
# 传入一个url的数组和一个数字,对url进行请求,并根据数字限制最大请求数
# 杂面试题
# 如果让你实现一个计算器,都需要考虑哪些问题
# 有一个数字字符串,它所代表的数字远大于js所能表示的最大数,可能有两万位,这样的数字你怎么判断它能不能被6整除。
能被6整除的数的特征 若一个整数能被2和3整除,则这个数能被6整除。 (1)能被2整除的数的特征 若一个整数的末位是0、2、4、6或8,则这个数能被2整除。 (2)能被3整除的数的特征 1、若一个整数的数字和能被3整除,则这个整数能被3整除。 2、由相同的数字组成的三位数、六位数、九位数……这些数字能被3整除。如111令3整除。 扩展资料: (1)能被8整除的数的特征 若一个整数的末尾三位数能被8整除,则这个数能被8整除。 (2)能被9整除的数的特征 若一个整数的数字和能被9整除,则这个整数能被9整除。 (3)能被10整除的数的特征 若一个整数的末位是0,则这个数能被10整除。 (4)能被11整除的数的特征 若一个整数的奇位数字之和与偶位数字之和的差能被11整除,则这个数能被11整除。11的倍数检验法也可用上述检查7的「割尾法」处理!过程唯一不同的是:倍数不是2而是1!
# 微前端
# 全文翻译功能
准备文件:自定义翻译的json对照数据。 思路一:节点遍历,通过以下函数实现: 将body下的文本节点中的searchWord, 替换为replaceWord,遍历json数据执行函数即可。
function replaceBodyText(searchWord, replaceWord){
var reg = new RegExp(searchWord, 'g');
function replaceNode(node){
node.childNodes.forEach(function(v){
if(v.nodeName === 'SCRIPT')
return; //排除<script>标签
if(!v.hasChildNodes()){
if(reg.test(v.textContent))
v.textContent = v.textContent.replace(reg, replaceWord);
return;
}
replaceNode(v);
});
}
replaceNode(document.body);
}
思路二:直接通过获取body内容来重写对照词汇,searchWord替换为replaceWord,遍历执行即可。
document.body.innerHTML = document.body.innerHTML.replace(/searchWord/g, "replaceWord");
# 如果需要你实现一个弹幕的组件,你需要如何设计这个组件的 props 和 state,内部如何实现,有哪些地方可以优化
# 现在有一个很大的项目,里面用了很多的发起请求的第三方包,现在要给它统一加日志,这个怎么做
复写一下window.xhr里的方法就行了 现在有fetch了,需要看下有没有
# 移动端适配都做过哪些
媒体查询,px2rem,flex,grid 微信中 小程序中 app中
# 换肤
# 国际化
# 有一个盒子,里面放一张大小不一定的图片,怎么让它填充
背景图片cover
还有js获取图片宽高再比较一下,再决定往哪个方向居中
img有个object-fit: cover
# 如果一个列表有很大量的数据,数据过多会导致dom过多然后页面卡顿,怎么解决
# requestAnimationFrame
由于屏幕种类,分辨率,屏幕尺寸的不同,屏幕自动刷新的频率不同,使用requestAnimationFrame可以自动适配屏幕刷新频率。避免丢帧。 与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。除此以外,还可以节省CPU,函数节流。
# 时间分片
function init () {
let ul = document.getElementById('container')
let total = 100000
let once = 20
// let page = total / once
let index = 0
function loop (curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
let pageCount = Math.min(curTotal, once)
window.requestAnimationFrame(function () {
let fragment = document.createDocumentFragment()
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
// setTimeout(() => {
// for (let i = 0; i < pageCount; i++) {
// let li = document.createElement('li')
// li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
// ul.appendChild(li)
// }
// loop(curTotal - pageCount, curIndex + pageCount)
// }, 0)
}
loop(total, index)
}
# 虚拟列表简易版
<!-- 物流详情页 -->
<!-- Create by luozheng on 2019/08/22 -->
<style lang="less" scoped>
@mainColor: #3db657;
.container {
width: 100%;
height: 100%;
}
.list-view {
height: 400px;
overflow: auto;
position: relative;
border: 1px solid #aaa;
}
.list-view-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.list-view-item {
padding: 5px;
color: #666;
line-height: 30px;
box-sizing: border-box;
}
</style>
<template>
<div
class="list-view"
@scroll="handleScroll">
<div
class="list-view-phantom"
:style="{height: contentHeight}"
>
</div>
<div
ref="content"
class="list-view-content"
>
<component
:is="comp"
class="list-view-item"
:style="{ height: itemHeight + 'px'}"
v-for="(item, index) in visibleData"
:key="comp + index"
:content="item"
>
</component>
</div>
</div>
</template>
<script>
const GoodsItem = () => import('@/components/base/ImgTitleArrowBox')
export default {
name: 'ListView',
components: {GoodsItem},
data () {
return {
visibleData: []
}
},
props: {
data: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 30
},
comp: {
default: ''
}
},
computed: {
contentHeight () {
return this.data.length * this.itemHeight + 'px'
}
},
mounted () {
this.updateVisibleData()
},
methods: {
updateVisibleData (scrollTop) {
scrollTop = scrollTop || 0
const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight) // 取得可见区域的可见列表项数量
const start = Math.floor(scrollTop / this.itemHeight) // 取得可见区域的起始数据索引
const end = start + visibleCount // 取得可见区域的结束数据索引
this.visibleData = this.data.slice(start, end) // 计算出可见区域对应的数据,让 Vue.js 更新
if (end === this.data.length) {
this.$emit('onBottom')
}
this.$refs.content.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)` // 把可见区域的 top 设置为起始元素在整个列表中的位置(使用 transform 是为了更好的性能)
},
handleScroll () {
const scrollTop = this.$el.scrollTop
this.updateVisibleData(scrollTop)
}
}
}
</script>
# 你的优点缺点
# 组件库的开发
# 传统前端开发和框架开发的区别是什么
# 离职原因
# 怎么接触前端
# 未来的职业规划
# 有什么想问的
技术栈、业务、进去负责什么、部门规划,加班情况
# 最有成就感的事情
# 这份工作给你带来了怎样的提升
# 期望待遇是多少
# 介绍一下项目 有哪些是由你主导提出的方案做的事情
准备几个项目进行详细讲解
# 你觉得你的优点是什么
# 技术和业务方向
# 跳的都比较频繁
# 这次找工作主要看重什么
# 之前的开发模式是怎样的,是一个人负责一个模块还是按照需求排期分配
# 你们做的是一个怎么样的产品
# 你对serverless的理解是什么样的
# 介绍一些上一份工作主要都负责哪些事情
# 之前的工作在每个阶段给你带来了哪些成长
# 如果你还在之前的部门的话,你有哪些事情是还想做的
# 研发流程上有做效率工具么
# 有没有做过哪些和代码没关系的但是比较精通的事情
# 介绍一下你最近读过的一本书
# 用三个正面的词和三个负面的词评论一下你自己
# 你对下一份工作的期望是怎样的
# 对上家公司的感受,自己的成长,不满的地方
# 对下一份工作有怎么样的期望,你对这个规划做过哪些努力
# 有没有经历过比较痛苦的阶段
# 做好一个产品工程师或者软件工程师,核心在于哪里
# 有用到敏捷开发么,对代码质量保障效果如何
https://www.zhihu.com/question/361735554?sort=created