浏览器、计算机网络、网络安全
BS和CS应用
C/S : Client/Server , 客户端/服务器
B/S : Browser/Server , 浏览器/服务器
[1] 语言:
C/S: c,c++,
B/S: java,php,.Net,node.js
[2] 更新:
C/S: 下载新版本的客户端,升级不大方便。
B/S: 热更新,永远都是最新的。
[3] 数据通信:
C/S: 基于自定义的应用层协议
B/S: 基于http协议,基于http的服务器拿来就能用,nginx,apache,微软的IIS这些
[4] 跨平台:
C/S: 开发时可能需要考虑跨平台问题(不同操作系统下)
B/S: 开发时跨平台方便,毕竟每个平台都有浏览器
[5] 数据处理:
C/S: 支持离线,数据可以本地保存或处理.
B/S: 支持云端,数据保存在云端,随时随地联网就能访问
跨域
所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
JSONP是在客户端写好使用服务端数据的回调函数。然后在script标签内调用
CORS跨域资源共享,在服务器端使用response.setHeader('Access-Control-Allow-Origin' ...)
代理服务器,webpack提供代理
JS函数性能测试
performance.now()
高性能API通过其函数 performance.now() 提供对 DOMHighResTimeStamp 的访问,该函数返回自页面加载时间(以毫秒为单位),精度最高为 5µs(以分数为单位)。
因此在实践中,你需要获取两个时间戳,将它们保存在变量中,然后用第一个时间戳减去删除第二个时间戳:
const t0 = performance.now();
for (let i = 0; i < array.length; i++)
{
// some code
}
const t1 = performance.now();
console.log(t1 - t0, 'milliseconds');
console.time(timeEnd)
function test1(){
console.time(1)
for(var i = 0 ; i<1000000 ;i++){
if(i==99) {
var temp=1
continue
}
}
console.timeEnd(1)
}
test2()//0.09326171875ms
浏览器从输入url到页面展示中间发生了什么(及相关优化)
- URL解析及编码。浏览器判断输入的URL是搜索内容还是URL地址。搜索内容则搜索内容+默认搜索引擎合成URL,URL地址,则加上协议,合并成完整的URL。URL规定只能有字母和数字和特殊符号。编码阶段就是把用户输入的URL地址中的中文和空格进行编码。
- 检查缓存。浏览器会把URL发送给网络进程。网络进程会先查找本地缓存是否缓存了该资源,如果有且在有效期内,会直接返回资源给浏览器进程。如果没有的话,直接进入网络请求流程
- DNS解析。就是根据浏览器识别出来的URL地址中的域名,到DNS服务器上,查找服务器外网IP的过程。DNS解析也是有缓存的:浏览器解析过一次,一般就会在本地记录一下解析记录。所以每一次DNS解析:本地DNS服务器解析(递归)、根/顶级/权威域名服务器解析(迭代)。递归查询是以本地名称服务器为中心查询, 递归查询是默认方式,迭代查询是以DNS客户端,也就是客户机器为中心查询。
在客户端输入URL后,会有一个递归查找的过程,从浏览器缓存中查找 -> 本地的hosts文件查找 -> 本地DNS解析器缓存查找 -> 本地 DNS服务器查找,在这个过程中任何一步找到了都会结束查找流程。如果在本地DNS服务器中也没有查询到,则根据本地DNS服务器 设置的转发器进行查询。
- 建立TCP连接,三次握手。首先,判断是不是https,如果是,则HTTPS其实是HTTP + SSL/TLS 两部分组成,也就是在HTTP基础上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。
- 数据传输。TCP 连接建立之后,浏览器端会构建请求行、 请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。如果是 HTTPS,还需要进行 TSL 协商。服务器接收到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304;若无效则重新返回资源,状态码为200。
- 关闭TCP连接,四次挥手
- 浏览器渲染
- 渲染进程将HTML内容转换为浏览器能够读懂的DOM树结构。
- 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分图块,并栅格化将图块转换成位图。
- 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。
浏览器缓存
浏览器缓存机制有四个方面:Memory Cache, Service Worker Cache, HTTP Cache, Push Cache
Cookie和Session
区别:
- 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
- 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
- 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
- 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
- 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。
两者配合使用来识别
Cookie的一些属性
H5缓存
H5新增的,两者统称为web storage
本地存储 local storage:持久化、有上限 5M
会话存储 session storage:浏览器关闭(会话结束),数据就销毁
localStorage作为H5的新添特征,与cookie的区别在于:
只存储在客户端本地,而不会随http请求发送到服务器端
并且只能通过手动删除缓存来清除,不能设置失效时间
sessionStorage和localStorage两者虽然对存储的内容比cookie(4k左右)大得多(5M左右),但是存入的东西都被转换成了字符串,也就是说无法存入数组或者对象,就算存入了也会被转化为字符串。
HTTP 缓存
HTTP缓存机制是根据HTTP报文的缓存标识进行的。HTTP缓存分为强缓存和协商缓存。强缓存优先级更高,在命中强缓存失败的情况下,才会走协商缓存。
- 强缓存
强缓存是利用http头中的Expires和Cache-Control两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的Expires和Cache-Control判断目标资源是否“命中”强缓存,如果命中则直接从缓存中获取资源,不会再和服务器发生通信。命中强缓存的情况下,返回的HTTP状态码为200.
Expires:过去(HTTP 1.0)的一种实现强缓存的方法,当服务器返回相应时,会在Response Headers中将过期时间写入Expires字段。Expires的时间戳由服务规定,但是和expires对比的本地时间来自本地客户端。所以对客户端时间和服务器时间一致性要求高。
Cache-Control: (HTTP 1.1新特性)。包含了很多值:
- public:表明响应可以被任何对象缓存(包括发送请求的客户端、代理服务器)
- private:表明响应只能被客户端缓存
- no-cache:跳过强缓存,直接进入协商缓存阶段。
- no-store:表示当前请求资源禁用缓存
- max-age=:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)
- s-maxage=:覆盖max-age或者Expires头。如果s-maxage未过期,则向代理服务器请求其缓存内容。(仅在代理服务器中生效)
优先级Cache-Control > Expires
- 协商缓存
协商缓存,也称为对比缓存。协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304。
同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified和Etag,其中Etag的优先级比Last-Modified高。
HTTP状态码
5类。200,204,206。301,302,303,304,307。400,401,403,404。500,503.
为什么外链css要放在头部,js要放在尾部?
移动设备横屏模式又被称为风景模式,竖屏模式又被称为肖像模式
心跳和轮询
JavaScript
ES6
let const
let:
虽然是块级作用域,但是不影响作用域链
const:
一定要赋初始值. 对数组和对象的元素修改,不算对常量发生改变
暂时性死区(TDZ)
ES6 规定,如果代码区块中存在 let 和 const 命令声明的变量,这个区块对这些变量从一开始就形成了封闭作用域,直到声明语句完成,这些变量才能被访问(获取或设置),否则会报错ReferenceError。这在语法上称为“暂时性死区”(英temporal dead zone,简 TDZ),即代码块开始到变量声明语句完成之间的区域。
和var的区别(面试回答)
for(var i = 0; i < 5; i++){
setTimeout(()=>console.log(i), 0);
}// 5 5 5 5 5
同步代码(for循环)执行结束之后,引擎才会处理异步任务。此时再去查找i的值,而此时i已经变成了5.
改为let之后,每一个for循环执行的时候都会保留一个i。
解构赋值
//基础用法:交换变量值,而不需要临时变量
let a = 1, b = 2;
[a, b] = [b, a];
//两个以上变量值的交换
let a = 1, b = 2, c = 3;
[a, b, c] = [c, b, a]
//快速提取数组中的元素
let arr = [1, 2, 3, 4, 5];
let [num1, num2, num3] = arr;
//提取数组中的某一个,某几个元素
let [, , nums3, ,nums5] = arr;
let {2: nums3, 4: nums5} = arr;
对象相关
对象的简化写法
- 属性名和值一致时可以只写一次
- 在对象中定义函数的时候,可以不用写
: function
let name = ...
let change = function(){
...
}
//ES6之前的写法
const school = {
name: name,
change: change,
lesson: function(){
......
}
}
//ES6
const school = {
name,
change,
lesson(){
......
}
}
函数相关
箭头函数
箭头函数和普通函数的异同
- 基本语法不同
- 箭头函数不会创建自己的this,它会从自己的作用域链(静态)的上一层继承this (静态,函数定义的时候就确定了)
- 箭头函数继承而来的this指向永远不变。不能通过
.call()/.apply()/.bind()方法动态修改函数this的指向 - 箭头函数不能作为构造函数使用
- 箭头函数没有自己的argments。接收多个参数可以使用拓展运算符
- 箭头函数没有原型prototype
箭头函数的简写
- 省略小括号,当形参有且只有一个的时候
- 省略花括号,当代码体只有一条语句的时候,此时return必须省略
函数参数的默认值
- 当参数为不传或传入undefined时使用默认值
- 具有默认值的参数,一般位置要靠后
rest参数
- 用于获取函数的实参,用来代替arguments。arguments是伪数组,实际是一个对象。rest参数就是一个数组,可以使用数组方法。
- 也是需要放到最后
function data(...args){
console.log(args);
}
data('a', 'b', 'c', 'd') //['a', 'b', 'c', 'd']
拓展运算符
-
...拓展运算符能将数组转换为逗号分隔的参数序列 -
拓展运算符的运用场景很多
- 数组的合并
- 数组的克隆
- 将伪数组转为真数组等等
模版字符串
- 可换行,也因此常用于定义 HTML 模板
- 可使用插值表达式添加变量,或是可执行的JS表达式
let str = `生成一个随机数:${ Math.random() }`
新的数据结构和数据类型
Symbol
- Symbol值是唯一的
- Symbol值不能与其他数据进行运算
- Symbol定义的对象属性不能使用for...in循环遍历, 但是可以使用
Reflect.ownKeys获取对象的所有键名
//创建Symbol
let s = Symbol();
consol.log(s, typeof s) // Symbol() "symbol"
//描述字符串创建
let s2 = Symbol('string'); // Symbol(string) "symbol"
let s3 = Symbol('string');
console.log(s2 === s3) //false
//Symbol.for创建
let s4 = Symbol.for('s')
let s5 = Symbol.for('s')
console.log(s4 === s5) //true
作用
向对象中添加独有的属性和方法,不知道原本的对象中是否已有该属性或方法。使用Symbol()可以直接加
Symbol内置值
ES6提供了11个内置的Symbol值,整体作为对象的属性
- Symbol.hasInstnce
迭代器 Iterator
迭代器是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。
在对象中表现为 Symbol.iterator 属性
-
ES6的新遍历命令 for...of 循环,Iterator接口只要供其使用
-
原生具备iterator接口的数据结构
- Array
- Arguments
- Set
- Map
- String
- TypedArray
- NodeList
在对象中手写迭代器
工作原理:
- 创建一个指针对象,指向当前数据结构的起始位置
- 第一次调用对象的next方法,指针自动指向数据结构的第一个成员
- 接下来不断调用next方法,指针一直向后移动,直到最后一个成员
- 每次调用next方法,会返回一个包含 value 和 done 属性的对象。最后一次会返回
{value: undefine, done: true}
const obj = {
name: "duixiang",
arr: [
'a',
'b',
'c',
'd'
],
//手动添加Symbol.iterator方法
[Symbol.iterator](){
let index = 0;
let _this = this;
return{
next: function(){
if(index< _this.arr.length){
const result = { value: _this.arr[index], done: false };
index++;
return result;
}else{
return { value: undefined, done: true };
}
}
}
}
}
//想要遍历对象中的数组,且符合面向对象的思想,就需要在对象中手动添加Symbol.iterator方法
for(let x of obj){
console.log(x)
}
生成器 generator
用于处理异步调用回调嵌套的问题
在函数名前面加一个"*",函数就变为生成器函数,执行该函数时,里面的函数不会立即执行,而是会返回一个生成器对象,调用生成器对象的.next方法函数开始执行,当遇到”yield“关键字,函数会停止执行,并把yield的值当做next方法返回对象的value; 当下次调用next方法时,函数从当前位置开始继续执行
function *geFn () {
console.log('111')
yield 100
console.log('222')
yield 200
console.log('333')
yield 300
}
let generator = geFn()
console.log(generator.next()) //111 {value: 100, done: false}
console.log(generator.next()) //222 {value: 200, done: false}
console.log(generator.next()) //333 {value: 300, done: false}
应用生成器异步编程解决回调地狱
//1s 后控制台输出 111,2s 后控制台输出 222,3s 后控制台输出 333
//回调地狱
setTimeout(()=>{
console.log(111);
setTimeout(()=>{
console.log(222);
setTimeout(()=>{
console.log(333);
}, 3000)
}, 2000)
}, 1000)
//generator生成器实现
function one(){
setTimeout(()=>{
console.log(111);
generator.next();
}, 1000)
}
function two(){
setTimeout(()=>{
console.log(222);
generator.next();
}, 2000)
}
function three(){
setTimeout(()=>{
console.log(333);
generator.next();
}, 3000)
}
function * gen(){
yield one();
yield two();
yield three();
}
let generator = gen();
generator.next();
应用生成器生成自增id
function * generateID() {
let id = 0;
while(true) {
yield id++
}
}
const genBookId = () => generateID.next().value
Promise
Promise是一种异步编程的解决方案,是由社区最早提出和实现。在ES6中被写进了语法标准。语法上Promise是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果。
见JS异步编程
集合 Set
内部有iterator,可以使用...和for...of。
.add
.delete
.has
.clear
JS 数据结构
栈(stack) 、堆(heap)、 队列(queue)是 js 的三种数据结构
栈和队列不用多讲。堆的特点是 无序 的 key-value 键值对 存储方式。
堆的存取方式跟 顺序 没有关系,不局限 出入口。
javascript代码在执行的时候会将不同的变量存于内存中的不同位置 栈内存:存放原始数据类型 以及 引用数据类型的指针 堆内存:存放引用数据类型的值
深入理解堆栈内存
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了,内存栈里面的数据就清空了(闭包是个例外,内存栈销毁了,但是闭包如果用到了内存栈里面的数据,闭包会把数据存下来。所以说: 闭包不能乱用) 。因此,所有在方法中定义的变量都是放在栈内存中的;
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是 堆内存 。堆内存 中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用,则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在某个时机的时候回收它。(具体看 JS运行环境 的垃圾回收机制)
JS数据类型
JS 中七种内置类型(null,undefined,boolean,number,string,symbol,object)又分为两大类型
记忆:U SO NB
- U undefined
- S string symbol
- O object
- N null number
- B boolean
基本数据类型和引用数据类型
null和undefined的区别
基本和引用的区别: 存储、值的可变性、比较时的不同
怎么判断数据类型
typeof 对于基本类型,除了 null 都可以显示正确的类型,对于 null 来说,虽然它是基本类型,但是会显示 object. typeof 对于对象,除了函数都会显示 object.
获得一个变量的正确类型,可以通过 Object.prototype.toString.call(xx)。这样我们就可以获得类似 [object Type] 的字符串。
instanceof
JS数组内存不连续
像 C/C++ 这种传统的编译型的语言,它们的数组在内存中用一串连续的区域来存放一些值,而且它们的数组中存放的数据类型都需要预先设定成同一类型。
而对于JS来说的话,数组中存储的数据类型是可以完全不一致的,这就意味着,JS 数组中内存地址不是连续的。
不过,现在的 JS 引擎为了优化 JS 的性能,它会分配一个连续的内存空间给存储了相同数据类型的数组,以达到更好的遍历效果。所以,只要你数组里存的是相同类型的值,在内存中的地址还是连续的。
JS浮点数精度问题
两个浮点数相加或相减可能会导致精度丢失问题
JS中的小数采用的是双精度(64位)表示的,由三部分组成:符 + 阶码 + 尾数,在十进制中的 1/10,在十进制中可以简单写为 0.1 ,但在二进制中,他得写成:0.0001100110011001100110011001100110011001100110011001……(后面全是 1001 循环)。因为浮点数只有52位有效数字,从第53位开始,就舍入了。这样就造成了“浮点数精度损失”问题。
浮点数问题解决方案
扩大倍数法:有多少位小数就扩大10的n次方
四舍五入法
//扩大倍数法
document.write((0.01*100+0.09*100)/100); //输出结果为0.1
//四舍五入法
document.write((0.01+0.09).toFixed(2)); //保留2位小数,输出结果为0.10
document.write(Math.round((0.01+0.09)*100)/100); //输出结果为0.1
转二进制
float转int的几种方法
Math.ceil()
Math.floor(),
Math.round(),
parseInt():用法,什么时候返回NaN,两个参数的含义。MDN
位运算:当需要正负数都向0取整时。这是由于js内部的类型自动转换,js数值都是由64位浮点型表示的,当进行位运算的时候,会自动转换为32为有符号的整数,并舍弃小数位。所以就可以实现向下取整了。
console.log(0.8 | 0);//输出0
console.log(-0.8 | 0);//输出0
console.log(1 | 0);//输出1
深拷贝、浅拷贝
这个概念是针对引用数据类型的,基本数据类型在赋值时会自动开辟新的存储空间。
浅拷贝实现方法
可以通过简单的赋值实现
function simpleClone(initalObj) {
let obj = {};
for ( let i in initalObj) {
obj[i] = initalObj[i];
}
return obj;
};
Object.assign()实现,多层对象嵌套时
var obj = { a: {a: "hello", b: 21} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"
//只有一层对象时,Object.assign()是深拷贝
var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }
函数库lodash的_.clone方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
展开运算符...
Array.prototype.concat() 和Array.prototype.slice()
深拷贝实现
手动复制
Object.assign()实现,(只有一层对象时)
let obj1 = {a: 1, b: 2}
let obj2 = Object.assign({}, obj1)
转JSON字符串再转成对象
//可以实现除function, Symbol(), undefined这三种之外的深拷贝
//对象中的这三种数据类型会造成数据丢失
let obj1 = {
fun: function(){},
aaa: undefined,
sym: Symbol(),
ccc: "aa"
}
console.log(JSON.parse(JSON.stringify(test))) //{ccc: 'aa'}
函数库lodash的_.cloneDeep方法
递归拷贝
//手写递归深拷贝,支持Array, Data, RegExp, DOM
function deepCopy(params) {
//如果不是对象则退出(可停止递归)
if(typeof params !== 'object') return;
//判断是数组还是普通对象来创建初始值
let newObj = (params instanceof Array) ? [] : {};
//循环对象属性
for(let i in params) {
//筛选一下自身属性
if(params.hasOwnProperty(i)){
//当前属性还未存在于容器(新)对象时
if(!(i in newObj)){
//分别判断Data, RegExp(正则表达式), DOM节点
//用不同方式存储进容器(新)对象
if(params[i] instanceof Date){
newObj[i] = new Date(params[i].getTime());
} else if(params[i] instanceof RegExp){
newObj[i] = new RegExp(params[i]);
} else if ((typeof params[i] === 'object') && params[i].nodeType === 1){
let domEle = document.getElementsByTagName(params[i].nodeName)[0];
newObj[i] = domEle.cloneNode(true);
} else {
//排除过所有其他情况,只剩普通对象或普通属性
newObj[i] = (typeof params[i] === 'object') ? deepCopy(params[i]) : params[i];
}
}
}
}
return newObj;
}
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
const newObj = deepCopy(source);
Object.create
JQuery的$.extend方法
Javascript的垃圾回收机制
两种常见的GC(Garbage Collection)算法
-
引用计数
- 当一个对象有一个引用指向它时,那么这个对象的引用就加1,并且将其引用次数保存起来,而当一个对象的引用为0时,那么这个对象就可以被销毁了(回收)。
- 实例代码中,person1的引用次数为3;person2和person3的引用次为1;
let person1 = { name: 'curry' } let person2 = { name: 'kobe', friend: person1 } let person3 = { name: 'klay', friend: person1 }- 弊端:不能处理循环引用的情况
let person1 = {
name: 'curry',
friend: person2
}
let person2 = {
name: 'klay',
friend: person1
}
-
标记清除
- 这个算法设置了一个根对象(root object) ,GC会定期从这个根对象开始往下查找有引用到的对象,而对于那些没有引用到的对象,也就是没有查找到的对象,就认为是需要进行回收的对象。
Array
直接修改原数组:头尾增删4个,排序反转2个,splice。这几个同时也是Vue中可以直接修改数据的数组api
不改变原数组...:concat, join, slice, map,filter,forEach,some,every,reduce等不改变原数组
数组去重
//利用ES6新数据类型,集合Set去重
function unique (arr) {
return Array.from(new Set(arr))
}
//利用for嵌套for,然后splice去重(ES5中最常用)
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;
}
//map
对象
API
let person = {
name: 'sxy',
}
Object.defineProperty
给对象添加属性。(添加属性的对象, 添加的属性名, 配置项)
Object.defineProperty(person, 'age', {
value: 23,
enumerable: true, //控制添加的属性是否可以枚举,默认值为false
writable: true, //控制属性是否可以被修改,默认值是false
configurable: true, //控制属性是否可以被删除,默认值false
//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值。
//为了age的值随number变化
get(){
return number;
},
set(value){
number = value;
}
})
对象的valueOf()与toString()
. 和 [] 访问对象属性的区别
for...of 和 for...in的区别
- 遍历数组时,两者都可用。
for...of遍历value,for...in遍历key
const arr = [10, 20, 30]
for(let index in arr){
console.log(index) // 0 1 2
}
for(let val of arr){
console.log(val) // 10 20 30
}
- 遍历对象时,
for...of不可用,for...in可用. 究其原因就是for...of是ES6新增的迭代器Iterator的衍生物。而对象并不原生具备Iterator。
const obj = {
name: 'sxy',
city: 'sheffield',
age: 23
}
for(let index in obj){
console.log(index) //name city age
}
for(let index in obj){
console.log(obj[index]) //sxy sheffield 23
}
for(let val of obj){
console.log(val) //Uncaught TypeError: obj is not iterable
}
- 遍历Map,Set时。
for...of可用,for...in不可用。遍历Map时for...of会同时遍历键和值。for...in不可用因为这两种数据结构没有顺序。
const map = new Map([[1, 1], [2, 2]]);
const set = new Set([10, 20, 30]);
for(let num of set) {
console.log('for...of set', num) //
}
for(let num of map) {
console.log('for...of map', num)
}
函数
原型与原型链
- 每个函数都有一个prototype属性(显式原型), 它默认指向一个Object空对象(即原型对象)
- 原型对象中有一个属性constructor, 它指向函数对象
- 给原型对象添加属性(一般都是方法),函数的所有实例对象自动拥有原型中的属性。添加方法之后,默认指向的那个空对象就不空了
- 每个实例对象都有一个proto属性(隐式原型)
- 对象的隐式原型的值为其对应构造函数的显式原型的值,也就是也指向那个Object空对象
原型链(隐式原型链)
访问一个对象的属性时,现在自身属性中查找,找到返回。如果没有,再沿着原型链向上查找。如果最终都没有找到,返回undefined
静态方法,实例方法,原型方法
| 方法类别 | 构造函数调用 | 实例化对象调用 |
|---|---|---|
| 静态方法 | 可以 | 不 |
| 实例方法 | 不 | 可以 |
| 原型方法 | 不 | 可以 |
添加方法,实操
// 初始化构造函数
const Parent = function() {
// 添加实例方法
this.instanceFunc = function() {
console.log('可以访问实例方法');
}
}
// 添加静态方法
Parent.staticFunction = function() {
console.log('可以访问静态方法');
}
// 添加原型方法
Parent.prototype.protoFunc = function() {
console.log('可以访问原型方法');
}
// 生成实例化对象
const parent = new Parent()
执行上下文与执行上下文栈
变量提升和函数提升
变量、函数声明提升,但是赋值不会提升。
顺序:先执行变量提升,再执行函数提升
执行上下文
全局和函数(局部)
- 全局执行上下文
在执行全局代码之前,将window确定为全局执行上下文
对全局数据进行预处理:var定义的全局变量添加为window的属性,function声明的全局函数添加为window的方法,把this指向window
- 函数执行上下文
在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
形参变量添加为执行上下文的属性
arguments(实参列表),添加为执行上下文的属性
this指向调用函数的对象
执行上下文栈
- 在全局代码执行前,JS引擎会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
- 在函数执行上下文创建后,将其添加到栈中(压栈)
- 在当前函数执行完之后,将栈顶的对象移除(出栈)
- 当所有的代码执行完之后,栈中只剩下window
作用域与作用域链
相对于上下文对象,是静态的。只要函数定义好了就会一直存在,且不会再变化。
执行上下文是动态的,函数调用时创建,函数调用结束后会自动释放执行上下文。
联系:执行上下文是从属于所在的作用域
闭包
当一个嵌套的内部子函数引用了嵌套的外部父函数中的变量时,就产生了闭包。不需要调用(内部)函数就会产生,定义的时候就产生了
常见的闭包
- 将函数作为另一个函数的返回值(函数柯里化常见)
- 将函数作为实参传递给另一个函数调用
但是实际上还是需要内部子函数引用外部父函数的变量
闭包的作用
- 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量、函数)
- 函数防抖、节流里的timer计时器变量,可以让多个函数共用同一个外部变量
闭包的生命周期
产生:在嵌套内部函数定义执行完成时就产生了(尤其注意不是在调用的时候)
死亡:在嵌套的内部函数成为垃圾对象时
闭包的缺点与解决
- 函数执行完后,函数内部的局部变量没有释放,占用内存时间会变长。造成内存占用
- 及时释放,能不用闭包就不用
内存溢出与内存泄露
- 内存溢出:(会崩溃)
当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误。
- 内存泄露:(不会出错)
占用的内存没有及时释放。内存泄露积累多了,就可能导致内存溢出
意外的全局变量、没有及时清理的计时器和回调函数、闭包
箭头函数和普通函数的异同
- 基本语法不同
- 箭头函数不会创建自己的this,它会从自己的作用域链的上一层即成this
- 箭头函数继承而来的this指向永远不变。不能通过
.call()/.apply()/.bind()方法动态修改函数this的指向 - 箭头函数不能作为构造函数使用
- 箭头函数没有自己的argments。接收多个参数可以使用拓展运算符
- 箭头函数没有原型prototype
函数柯里化
函数柯里化:指封装一个函数,接收原始函数作为参数传入,并返回一个能够接收并处理剩余参数的函数
function curring(){
let args = Array.prototype.slice.call(arguments);
let inner = function(){
args.push(...arguments);
return inner;
}
inner.toSum = function(){
return args.reduce(function(prev, cur){
return prev + cur;
})
}
return inner;
}
curring(1, 2, 3)(4).toSum()
Javascript的全局属性和全局函数
JavaScript全局属性和全局函数可以与所有内置JavaScript对象一起使用。
异步编程(Promise,宏、微任务)
JS引擎线程会维护一个 执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。
而JS引擎线程遇到异步任务,会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 (也叫分线程、浏览器的webapi)将异步对应的 回调函数 加入到消息(任务)队列中,消息队列中的回调函数等待被执行。
如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
异步编程的操作有哪些
- fs文件操作
fs.readFile('content.txt', (err, data)) => {
if(err) throw err;
console.log(data.toString());
}
- 数据库操作
- AJAX网络请求
- 定时器
- DOM事件监听
错误 Error
常见的内置错误
ReferenceError, TypeError, RangeError, SyntaxError
错误处理的两种方式
捕获错误:try{}catch(error){}
抛出错误(然后再捕获):throw error
错误对象的两个属性
message: 错误相关信息
stack: 函数调用栈记录信息 (错误发生在哪里)
- 给出一个可重数组,求最大的不连续子数组的个数。eg. 输入1 3 4 5 6 7 输出 4
- 最大子段和问题,即对于一个数组找出其和最大的子数组。允许在求该问题前,翻转该数组的连续一段,则翻转后的最大子段和是多少?eg. 输入 -1 3 -5 2 -1 3 输出 7
- 猜字段,谜底是s, 每次的尝试是t. 输入的内容第一行是s、t的长度和尝试的次数。下面的是每次尝试的t,后面的第一个数字代表位置对且数字对的个数,第二个数字代表数字对但位置不对的个数。输出可能的答案s中字典序最小的。
Promise基本使用方法
//1.创建一个新的promise对象
const p = new Promise((resolve, reject) => {//执行器函数
//2.执行异步操作任务
setTimeout(() => {
const time = Date.now()
//如果当前时间是偶数就代表成功,否则代表失败
if(time % 2 == 0){
//3.1 如果成功,调用resolve(value)
resolve('成功')
}else{
//3.2 如果失败,调用reject(reason)
reject('失败')
}
}, 1000);
})
p.then(
value => {//接收得到成功的value数据 onResolved
console.log('success', value)
},
reason => {//接收得到失败的reason数据 onRejected
console.log('fail', reason)
}
)
//输出 "fail" "失败" or "success" "成功"
util的promisify 方法
可以穿入一个常见的错误优先的回调的函数,并返回Promise的版本。就可以不用再手写了
const util = require('util');
const fs = require('fs');
let newReadFile = util.promisify(fs.readFile);
newReadFile('content.txt').then(value=>{})
Promise的优势
指定(成功或失败的)回调函数的方式更灵活
通过回调函数实现异步时(旧的):必须在启动异步任务前指定
function successCallback(result){
console.log("成功" + result)
}
function failureCallback(error){
console.log("失败" + error)
}
//使用纯回调函数创建异步任务
createAudioFileAsync(audioSettings, successCallback, failureCallback);
Promise:启动异步任务 => 返回promise对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)
- 如果先指定回调。当状态发生改变时,回调函数调用,得到数据
- 如果先改变状态,当指定回调函数时,回调函数调用,得到数据
支持链式调用,解决回调地狱问题
回调地狱在generator中也有例子。对照学习
var sayhello = function (name, callback) {
setTimeout(function () {
console.log(name);
callback();
}, 1000);
}
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
//输出: first second third end
//1.封装一个函数 : 根据文件名生成 文件读取的promise
function getPromise(fileName) {
let p = new Promise((resolve, reject) => {
//读文件
fs.readFile(`./data/${fileName}.txt`, 'utf-8', (err, data) => {
if (err == null) {
//成功
resolve(data);
} else {
//失败
reject(err);
}
});
});
return p;
};
//2.解决需求: 要先读a, 读完a后读b, 读完b后读c.
//开始读取a
getPromise('a').then((data)=>{
console.log(data);
//继续读取b
return getPromise('b');
}).then((data)=>{
console.log(data);
//继续读取c
return getPromise('c');
}).then((data)=>{
console.log(data);
}).catch((err)=>{
//以上三个异步操作,只要有任何一个出错,都会执行err
console.log(err);
});
Promise的API
-
执行器函数(resolve, reject) => {}.
-
Promise.resolve() 返回一个成功/失败的Promise对象。Promise
-
Promise.reject() 返回一个失败的Promise对象。
-
Promise.all() 返回一个Promise对象,是否成功取决于...。成功的结果是所有成功结果组成的数组,失败的结果是第一个失败的结果
-
Promise.race() . 第一个改变状态的promise对象的状态决定
-
Promise.then() . 参数为
(onResolved, onRejcted) => {}. 用于指定成功的回调和失败的回调。返回一个Promise对象。该Promise对象的状态和结果:取决于回调函数的执行结果(成功或失败的回调都成立)- throw一个错误,则为rejected,结果为抛出的错误
- 除Promise以外的任何对象,则为fulfilled,结果是
- 如果是Promise对象,那就是根据这个Promise对象的状态和结果
-
如果指定多个回调,改变状态后都会执行
-
Promise.catch()
链式调用
- 异常穿透。当多个.then()链式调用时,只需要在最后使用一次.catch()指定失败的回调
let p = new Promise((resolve, reject) => {
resolve('ok');
})
p.then(value => {
console.log(111)
}).then(value => {
console.log(222);
}).then(value => {
console.log(333);
}).catch(reason => {
console.warn(reason);
})
- 中断。返回一个 pending 状态的Promise对象
p.then(value => {
console.log(111)
//中断唯一的方法
return new Promise(()=>{});
}).then(value => {
console.log(222);
}).then(value => {
console.log(333);
}).catch(reason => {
console.warn(reason);
})
手写Promise、Promise.retry
默写思路:
构造函数本体
- 构造函数Promise,接收执行器函数executor作为参数。两个属性,及初始值。
- 保存一下this,等会儿会用
- 写resolve和reject函数,修改两个属性。考虑到状态只能改一次,每次都得判断。
- 执行器函数直接执行。考虑到throw,把执行器函数用try{...}catch(e){reject(e)}包起来
class Promise{
//构造方法
constructor(executor){
//添加属性
this.PromiseState = 'pending';
this.PromiseResult = null;
this.callbacks = [];
//保存实例对象this的值
const self = this;
function resolve(value){
if(self.PromiseState !== 'pending') return;
//1.修改对象的状态(PromiseState)
self.PromiseState = 'fulfilled';
//2.设置对象结果值(PromiseResult)
self.PromiseResult = value;
//调用成功的回调函数
setTimeout(() => {
self.callbacks.forEach(item => {
item.onResolved(value);
})
});
}
function reject(reason){
if(self.PromiseState !== 'pending') return;
//1.修改对象的状态(PromiseState)
self.PromiseState = 'rejected';
//2.设置对象结果值(PromiseResult)
self.PromiseResult = reason;
//调用失败的回调函数
setTimeout(()=>{
self.callbacks.forEach(item => {
item.onRejected(reason);
})
});
}
try{
//执行器函数(同步调用)
executor(resolve, reject);
}catch(error){
reject(error);
}
}
then(onResolved, onRejected){
const self = this;
//判断回调函数参数(onRejected可以不传)
if(typeof onRejected !== 'function'){
onRejected = reason => {
throw reason;
}
}
if(typeof onResolved !== 'function'){
onResolved = value => value;
}
return new Promise((resolve, reject) => {
//封装重复使用的函数
//判断throw、是不是Promise...改变状态的那一套
//type:onRejected 或者是 onResolved
function callback(type){
try{
//获取回调函数的执行结果
let result = type(self.PromiseResult);
if(result instanceof Promise){
result.then(v => {
resolve(v);
}, r => {
reject(r);
})
}else{
resolve(result);
}
}catch(e){
reject(e);
}
}
if(this.PromiseState === 'fulfilled'){
setTimeout(() => {
callback(onResolved);
})
}
if(this.PromiseState === 'rejected'){
setTimeout(() => {
callback(onRejected);
})
}
/*
如果promise中包裹了一个异步操作,那就会先执行then方法,
而此时PromiseState状态还没有改变,所以还要加上pending时的操作
*/
if(this.PromiseState === 'pending'){
//保存回调函数,为了让resolve或reject内部调到这个回调函数
//因为异步操作时,回调函数是在promise状态改变时调用的。(同步时是在调用then方法的同时调用)
//也就是应该在上面resolve和reject函数中PromiseState改变后调用
//但又不能直接调用,所以需要把回调函数保存在对象内部(callback)
this.callbacks.push({
onResolved: function(){
callback(onResolved);
},
onRejected: function(){
callback(onRejected);
}
});
}
})
}
catch(onRejected){
return this.then(undefined, onRejected);
}
static resolve(value){
//返回promsise对象
return new Promise((resolve, reject) => {
if(value instanceof Promise){
value.then(v => {
resolve(v);
}, r => {
reject(r);
})
}else{
resolve(value);
}
});
}
static reject(reason){
return new Promise((resolve, reject)=>{
reject(reason);
})
}
static all(promises){
return new Promise((resolve, reject) => {
let count = 0;
let arr = [];
for(let i = 0; i < promises.length; i++){
promises[i].then(v => {
count++;
arr[i] = v;
if(count === promises.length){
resolve(arr);
}
}, r => {
reject(r);
})
}
})
}
static race(promises){
return new Promise((resolve, reject) => {
for(let i = 0; i < promises.length; i++)[
promises[i].then(v => {
resolve(v);
}, r => {
reject(r);
})
]
})
}
}
Promise.prototype.then = function(onResolved, onRejcted)
- .then返回的是一个Promise对象,所以以下代码都包在Promise里
- 根据状态判断执行哪个回调。回调接收上面的结果作为参数
- 此时应该考虑Promise执行器函数一般是异步的,所以指定回调时需要判断如果是pending状态,会怎么样。
- 如果是异步改变状态,就需要在改变状态的地方也就是上面构造函数里执行回调。但是回调函数是then的参数,不能直接调用
- 再增加一个属性作为保存回调函数的容器 callback 。在then判断为pending时,将回调通过该容器保存。在上面判断是否保存有回调,有的话直接执行
- 多个.then指定多个回调,都会执行。同步是满足该条件的,异步时,容器得是一种可以保存多个回调的数据结构,数组。把每次.then指定的回调做成对象保存在数组里. 调用的时候就得用forEach调用了
- 再写对.then方法返回的Promise对象的处理。三种情况
/* 实现Promise.retry方法,重试异步函数
异步函数成功时,resolve()。失败时重试,超过一定重试次数才会reject() */
function fn() {
const n = Math.random();
return new Promise((resolve, reject) => {
setTimeout(() => {
if(n > 0.7) {
reject(n);
}else{
resolve(n);
}
}, 1000)
})
}
Promise.retry = (func, chances) => {
new Promise(async (resolve, reject) => {
while(chances--){
try{
const res = await func();
console.log("执行成功,结果是" + res);
resolve(res);
break;
}catch(error){
console.log("执行失败,结果是" + error);
if(!chances){
reject(error);
}
}
}
}).catch(() => {
console.log("全部尝试次数都失败");
})
}
Promise.retry(fn, 4);
async, await
async函数
- 函数的返回值是promise对象
- promise对象的结果由async函数执行的返回值决定。非promise成功.....
await表达式
- await右侧的表达式一般是promise对象,但也可以是其他的值
- 如果表达式是promise对象,await返回promise成功的值
- 如果promise是失败的,就会抛出异常,此时需要try...catch进行处理
- 如果表达式是其他值,直接将其作为await的返回值
注意点
- await必须写在async函数中,但async函数可以不写await
宏任务、微任务
异步任务都是先放在任务队列里。任务队列分为宏队列和微队列。优先级微队列 > 宏队列
-
宏任务
- DOM事件回调
- AJAX请求回调
- 定时器回调
-
微任务
- promise回调
- mutation回调
其它
JS防抖和节流
防抖
在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms
- 如果在200ms内没有再次触发滚动事件,那么就执行函数
- 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时
效果:如果短时间内大量触发同一事件,只会执行一次函数(最后一次)。
实现:
- 需要用到 在函数里面返回函数,否则
debounce(payMoney)会变成立即执行 - 要想定时器timer唯一,需要用到闭包,多个函数共用这一个timer
- this指向的问题,在不使用防抖函数时,回调函数
payMoney中的this指向点击按钮。使用防抖函数之后,回调函数的this指向了Window。因此在setTimeout前保存指向按钮的this。在后面执行payMoney的时候使用.call人为修改this指向 - 传入参数的问题,
payMoney需要传入参数时,使用arguments,并将.call改为.apply,不能用箭头函数 - timer是点击的次数
const button = document.querySelector('input');
function payMoney(){
console.log('已支付');
console.log(this);
}
function debounce(func, delay){
let timer;
return function(){
//let context = this;
if(timer){
clearTimeout(timer);
timer = setTimeout(function(){
func();
//func.call(context);
}, delay)
}else{
timer = setTimeout(function(){
func();
//func.call(context);
}, delay)
}
}
}
button.addEventListener('click', debounce(payMoney, 1000));
节流
也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活
效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。
实现:
function coloring () {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
document.body.style.background = `rgb(${r},${g},${b})`
}
function throttle (func, delay){
let timer;
return function(){
let context = this;
if(timer) return;
else{
timer = setTimeout(function(){
func.apply(context, arguments);
timer = null;
}, delay)
}
}
}
window.addEventListener('resize', throttle(coloring, 0));
正则表达式
面试中会遇到的正则题
正则表达式浏览器版本匹配
正则 url
匹配十六进制颜色代码:
JS输出各类随机数
cloud.tencent.com/developer/a…
JSON
字符串必须用双引号,不能使用单引号。
null可以,undefined、NaN、Infinity都不可以
Javascript对象和JSON之间互相转换的方法:
JSON.parse() //JSON转JS对象
JSON.stringify() //JS对象转JSON
第七章 事件
事件冒泡和事件捕获
总结:事件流的模型是自上而下捕获,到达目标,然后再自下而上冒泡。 1.事件捕获阶段 2.处于目标阶段 3.事件冒泡阶段
对于dom0级btn.onclick dom2级btn.addEventListener('click',fn,false) 都是默认事件冒泡。其中dom2级第三个默认false,改为true就变为事件捕获。
同时存在的优先级:捕获 > 冒泡
事件对象event的公共属性和方法
e.stopPropagation() //阻止冒泡和捕获
e.preventDefault() //取消默认行为
AJAX
AJAX基本知识
AJAX优缺点
优点:
- 可以无需刷新页面而与服务器端进行交互
- 允许根据用户事件来更新部分页面内容
缺点:
- 没有浏览历史,不能回退
- 存在跨域问题()
- SEO不友好
HTTP 报文结构
请求报文
- 行 POST /s?ie=utf-8. HTTP/1.1
- 头 Host: Baidu.com Cookie: name=sxy Content-type:...... User-Agent: chrome 83
- 空行
- 体 username=admin&password=admin
响应报文
- 行:HTTP/1.1 200 OK
- 头: Content-Type, Content-length, Content-encoding:
- 空行
- 体
HTTP请求类型及区别
Post,Delete,Put,Get分别对应增删改查
GET和POST的区别
-
作用不同,见上方增删改查
-
传参方式不同
- GET的参数一般是通过 ? 跟在URL后面,多个参数则通过 & 连接
- POST的参数一般包含在请求体内
-
由于传参方式不同,GET比POST安全性更差。实际上两者都不安全,请求体也是明文传输,需要使用HTTPS
-
也是由于传参方式不同,GET传参的数据量一般被限制为 <= 2kb. POST一般没有限制
其它区别见
AJAX使用
模拟服务器代码
Express,Node.js的Web开发框架
//1.引入express
const express = require('express');
//2.创建应用对象
const app = express();
//3.创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get('/server', (request, response)=>{
//设置相应头 允许跨域
response.setHeader('Access-Control-Allow-Origin', '*');
//设置响应体。
//响应体只能传字符串,所以如果想要传对象,需要转换为JSON格式
const data = {
a: 'a1'
}
let str = JSON.stringify()
response.send('HELLO AJAX');
})
//4.监听端口启动服务
app.listen(8000, ()=>{
console.log("服务已启动,8000端口监听中")
})
AJAX基本使用
xhr的readystate属性
可选值:
- 0:未初始化
- 1:open方法调用完毕
- 2:send方法调用完毕
- 3:服务端返回了部分结果
- 4:服务端返回了全部结果
原生AJAX使用
const btn = document.getElementByTagName('button');
btn.onclick = function(){
//1.创建对象
const xhr = new XMLHttpRequest();
//设置超时时间,和超时的回调
xhr.timeout = 2000;
xhr.ontimeout = function(){
console.warn('网络异常,请稍后重试')
}
//2.初始化 设置请求方法和url
//xhrReq.open(method, url, async, user, password);
xhr.open('GET', 'http://127.0.0.1:8000/server');
//此时可以设置请求头,Content-Type用于设置请求体内容的类型,...是有固定写法的,用时查
//xhr.setRequestHeader('Content-Type', '...');
//3.发送 如果是POST方法且要传参,在此处设置请求体
//xhr.send('params')
xhr.send();
//4.事件绑定
//readystate是xhr对象中的属性。表示状态,具体可选值见上方
xhr.onreadystatechange = function(){
if(xhr.readystate === 4){
if(xhr.status >= 200 && xhr.status < 300){
//开始处理结果
console.log(xhr.status);//响应行 状态码
console.log(xhr.statusText);//响应行 状态字符串
console.log(xhr.getAllResponseHeaders);//响应头
console.log(xhr.response);//响应体
}else{
}
}
}
}
AJAX重复请求问题
xhr.abort()手动取消AJAX请求
//前一个请求没有结束时再次发送请求,取消上一次请求
axios
基于Promise的HTTP客户端,可以在浏览器或node.js环境下运行
Promise based HTTP client for the browser and node.js
功能:
- Make XMLHttpRequest from the browser (AJAX)
- Make http requests from node.js
- Supports the Promise API
- Intercept request and response, 请求拦截器和响应拦截器
- Transform request and response data
- Cancel requests
- Automatic transforms for JSON data
- Client side support for protecting against XSRF
请求拦截器和响应拦截器
内部原理就是Promise
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
Node.js
在Node中,一个js文件就是一个模块。
在Node中,每个js文件中的js代码都是独立运行在一个函数中的。所以一个模块中的变量和函数在其他模块中无法访问。
CommonJS:
所以需要通过exports暴露,将需要暴露的变量或方法设置为exports的属性即可。
引入:require(),该函数参数是引入的模块路径,函数返回一个对象,代表引入的模块
模块分为
核心模块:由node引擎提供的模块,核心模块的标识就是模块的名字
文件模块:由用户自己创建的模块,标识是文件路径
当node在执行模块中的代码时,它会将代码用这个函数包裹
function(exports, require, module, _filename, _dirname){
... //
}
这五个参数的意义分别是
- exports 该对象用来将变量或函数暴露到外部
- require 函数,用来引入外部的模块
- module 代表的是当前模块本身,第一个exports实际上就是它的属性 。
- _filename 当前模块的完整路径
- _dirname 当前模块所在文件夹的完整路径
module.exports和exports使用上有细微的区别。exports必须使用exports.的方式,module.exports既能用 . 也能直接赋值
在Node中有全局对象global,和网页中的window类似
包
符合规范的包结构应该包括以下文件:
- Package.json 描述文件 (必须,其它都是可选的)
- bin 可执行二进制文件
- lib js代码
- doc 文档
- Test 单元测试
JS模块化规范有哪些
AMD, CMD, CommonJS, UMD, ESM
CSS
rem 和 em 区别
rem的大小取决于根元素的字体大小。根元素字体大小乘以rem值
em的大小是em值乘以元素的字体大小
字体大小默认为16
继承
背景相关的,布局相关等的这些样式都不会被继承。
选择器
通配选择器、元素(标记)选择器、类选择器、ID选择器、属性选择器
复合选择器:交集选择器、并集选择器
关系选择器:子元素选择器、后代元素选择器、兄弟选择器
伪类选择器:伪类用来描述一个元素的特殊状态,比如:第一个子元素、被点击的元素、鼠标移入的元素.…
伪元素选择器:表示页面中一些特殊的并不真实的存在的元素(特殊的位置)
伪元素使用::开头
::first-letter表示第一个字母::first-line表示第一行::selection表示选中的内容::before元素的开始::after元素的最后::before和::after必须结合content属性来使用
a标签的四个伪类顺序
:link、:visited、:hover、:active
lvha
浏览器的就近原则,后面的样式会覆盖前面的。比如 hover 在 link 前面的话。不管鼠标怎么动,都只会显示link的样式。
选择器优先级顺序
!important > 行内(内联)样式 >
1.id选择器(#myid)
2.类选择器(.myclassname)
3.标签选择器(div,h1,p)
4.子选择器(ul > li)
5.后代选择器(li a)
6.伪类选择(a:hover,li:nth-child)
最后,需要注意的是:
!important的优先级是最高的,但出现冲突时则需比较”四位数“; 优先级相同时,则采用就近原则,选择最后出现的样式; 继承得来的属性,其优先级最低。
伪类和伪元素
伪类和伪元素的特性及其区别
常见的伪类及其作用
盒子模型
行内元素的上下padding、margin无效果
水平布局(计算宽度)
一个元素在父元素中,水平布局必须满足以下等式:
Margin-left + border-left + padding-left + width + padding-right + border-right + margin-right = 其父元素内容区的宽度
如果不满足,则称为过度约束,等式会进行自动调整:
如果七个值中没有auto,浏览器会自动调整margin-right的值让等式成立。所以无法手动设置margin-right。
七个值中有三个值可以设置成auto,被设置为auto的会自动把父元素充满
- width:
- Margin-left
- Margin-right
如果把一个宽度和一个外边距都为auto,宽度会填充满,外边距为0。如果宽度固定,两个外边距为auto,两个外边距会平分。这点也被用来让子元素在父元素中水平居中
垂直布局(overflow)
如果自元素的大小超出父元素,则子元素会从父元素溢出。此时就需要在父元素中设置overflow属性来决定如何处理溢出的子元素
- visible 子元素会正常溢出
- hidden 溢出的部分将会被裁减
- scroll 出现滚动条,可以完整查看子元素。
- auto 根据需要设置滚动条,哪里溢出哪里出现滚动条
外边距的折叠
垂直外边距的折叠。相邻的垂直方向外边距会发生重叠现象。
兄弟元素:外边距都是正数,会取两者间的最大值。如果一正一负,则取两者的和。都是负数,取绝对值大的。
父子元素:子元素的会传递给父元素。
盒子的大小
默认情况下,盒子可见框的大小由内容区、内边距、边框......
设置了box--sizing时:(也即盒子可见框大小的计算方式,也即设置width和height的作用)
- content-box (默认值),宽度和高度用来设置内容区的大小
- border-box, height和width用来设置整个盒子可见框的大小
浮动
通过浮动可以是一个元素向其父元素的左侧或右侧移动
float可选值:none, left, right
元素设置浮动之后,元素水平布局的等式就不需要强制成立了。而且会从文档流中脱离,不再占用文档流的位置。如果多个元素都浮动,就会并排排列。
BFC 块级格式化环境
BFC是一个CSS中的一个隐含的属性,可以为一个元素开启BFC。开启BFC后改元素会变成一个独立的布局区域
元素开启BFC之后的特点
- 开启BFC的元素不会被浮动元素所覆盖
- 开启BFC的元素,子元素和父元素外边距不会重叠
- 开启BFC的元素可以包含浮动的子元素
开启BFC的方法(前两种方式不推荐,副作用比较大)
- 设置元素的浮动
- 将元素设置为行内块元素
- 将元素的overflow设置为一个非visible的值,一般用hidden
- 绝对定位元素,position(absolute,fixed)
除了开启BFC之外,还有几个方法可以解决高度塌陷
- clear属性。也可以清楚浮动元素对当前元素所产生的影响。
clear属性的可选择:
left 清除左侧浮动元素对当前元素的影响
right ......
both 清除影响最大的那侧
- clear + after伪类(得改成块级盒子) ,完美,几乎没有副作用
clearfix
clearfix这个样式可以同时解决高度塌陷和外边距重叠的问题,当你在遇到这些问题时,可以直接使用
Position
可选值:
- static 默认值,没有开启定位
- relative 相对定位
- absolute 绝对定位
- fixed 固定定位
- sticky 粘滞定位
相对定位
- 元素开启相对定位以后,如果不设置偏移量元素不会有任何变化
- 相对定位是参照于元素在文档流中的位置进行定位
- 相对定位会提高元素的层级(重叠时盖住别的元素)
- 相对定位不会使元素脱离文档流(也即原先的位置依旧占位)。也不会改变元素的性质(行内、块)
绝对定位
- 开启后不设置偏移量元素的位置不会发生变化
- 开启绝对定位后,元素会从文档流中脱离
- 也会改变元素的性质,行内会变成块,且块的宽高不设置的话被内容撑开。(其实都是因为元素脱离文档流)
- 也会使元素提升一个层级
- 绝对定位时相对于其包含块进行定位的
水平布局
开启绝对定位后,水平方向的布局等式就需要添加left和right两个值了
left + margin-left + border-left + padding-left + width + padding-right + board-right + margin-right + right
当发生过度约束时,如果没有auto会自动调整right以满足等式
垂直布局
top + margin-top/bottom + padding-top/bottom + border-top/bottom + height + bottom
包含块
正常情况下:包含块就是离当前元素最近的祖先块元素
绝对定位的包含块:包含块就是离它最近的开启了定位的祖先元素。如果所有的祖先元素都没有开启定位,根元素就是它的包含块。
固定定位
- 固定定位也是一种绝对定位,大部分特点和绝对定位一样
- 唯一不同的是固定定位永远参照于浏览器的视口
粘滞定位
和相对定位的特点基本一致。不同的是粘滞定位可以在元素到达某个位置时固定。
元素的层级 z-index
对于开启了定位的元素,可以通过z-index属性来指定元素的层级。
一个整数作为参数,值越大元素的层级越高。层级一致,优先显示结构靠后的元素。
祖先的元素层级再高也不会盖住后代元素。
弹性盒(flex)
一种布局手段,主要用来代替浮动完成页面的布局
flex可以使元素具有弹性,让元素可以跟随页面的大小改变而改变。display: flex/inline-flex
弹性容器的直接子元素是弹性元素
flex-direction 指定容器中弹性元素的排列方式。可选值:
- row 默认值,弹性元素在容器中水平排列(左向右)
- row - reverse,水平排列(右向左
- column
- column-reverse
弹性元素的属性:
- flex-grow 指定弹性元素的伸展的系数,即当父元素有多余空间时,子元素按照什么比例伸展
- flex-shrink 当父元素的空间不足以容纳所有的子元素。默认值是1也就是等比例收缩
弹性布局中可以通过justify-content: center实现水平居中;通过align-items: center实现垂直居中
一些居中
要让一个文字在父元素中垂直居中,只需设置父元素的line-height,与父元素的height保持一致。
利用垂直、水平布局等式的居中:
水平居中:设置margin-left、margin-right为auto,且left、right为0(因为这两个默认值是auto,不设置时浏览器会优先调整这两个属性)。
垂直居中:类似的,设置margin-top/bottom为auto,且top、bottom为0。(必须得是绝对定位)
水平居中
- 行内元素,父元素text-align:center,即可实现水平居中。
- 块级元素,给该元素设置margin: 0 auto.
- 使用flex布局
.parent {
display: flex; /*设置弹性盒子*/
justify-content: center; /*设置对齐方式*/
}
CSS隐藏元素的方法
- opacity: 和 filter: opacity()
属性可以传递一个 0-1 之间的数字。或者0%到100%之间的百分比。对应地表示完全透明和完全不透明。
- color alpha 透明度
可以将元素的color、background-color 和 border-color 等属性设置为rgba(0,0,0,0),这样就会使元素完全透明
- transform
可以用 scale(0) (缩放0倍) 或者 translate(-9999px, 0px) (往右移动-9999,往下移动0)属性值来将元素隐藏
- clip-path: circle(0)
clip-path属性可以创建一个剪辑区域,用于确定元素的哪些部分是可见的。使用clip-path: circle(0)可以将元素进行隐藏。
- visibility: hidden
- display: none
- z-index
设置为负值,实际上就是将元素放在了我们看不到的层
- position
开启绝对定位,然后left: -999px
字体
font-family : 字体族(字体的格式)
可以直接设置某一个字体。也可以设置某一类字体。
- serif 衬线字体
- sans-serif 非衬线字体
- monospace 等宽字体
重绘与回流
基础知识
浏览器渲染网页的基本流程:
- 浏览器会把HTML解析成Document Object Model (DOM) ,把CSS解析成CSS Object Model (CSSOM) ,DOM和CSSOM合并就产生了Render Tree(渲染树) 。
- 有了Render Tree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
回流 Reflow
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变——resize事件发生时
- 元素尺寸(边距、填充、边框、宽度和高度)或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素(v-if)
- 激活CSS伪类(例如:
:hover) - 查询某些属性或调用某些方法(计算 offsetWidth 和 offsetHeight 属性)
- 设置 style 属性的值
v-if和v-show都会导致回流。v-if是删除或添加dom节点,v-show会将元素脱离或回归文档流。
重绘 Repaint
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility、opacity等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
回流必将引起重绘,而重绘不一定会引起回流。
过渡和动画
过渡(transition) 属性
- Transition-property:指定要执行过渡的属性。多个属性间使用 , 隔开。如果所有属性都要过渡,使用关键词 all。不能过渡auto
- Transition-duration:指定过渡效果的持续时间,2s 2ms...
- Transition-timing-function:过渡的时序函数。默认值ease,慢速开始,先加速再减速。linear:匀速运动。ease-in:加速运动。ease-out:减速运动。可以自己配置贝塞尔曲线控制。steps()分步执行过渡效果
- Transition-delay:和setTimeout一个效果
过渡做不到自动执行,必须发生某个行为之后才会发生。所以需要动画
设置动画效果,必须先设置一个关键帧。关键帧设置了动画执行的每一个步骤
.box{
animation-name: test;
animation-duration: 2s;
animation-delay: 1s;
animation-timing-function: ease;
/* 动画执行的次数*/
animation-iteration-count: number or infinite
/* 动画执行的方向 默认值normal,from => to. reverse相反。
alternate,alternate-reverse*/
animatin-direction:
/* 动画执行的状态 默认值running, paused暂停*/
animation-play-state:
}
@keyframes test {
/* from动画的开始位置 也可以用 0%*/
from{
}
/* 其中也可以额外设置其它阶段的状态 % */
/* to表示动画的结束位置 or 100%*/
to{
}
}
CSS3
媒体查询
使用它,您可以指定一个媒体查询和一个CSS块,当且仅当该媒体查询与正在使用其内容 的设备匹配时,该CSS块才能应用于该文档。
HTML
H5新标签
结构标签
-
标记定义一个页面或一个区域的头部
-
标记定义一个页面或一个区域的底部
-
标记定义导航链接
-
标记定义一个区域,文档中的节
-
标记定义页面内容部分的侧边栏
-
标记定义一篇文章
-
标记定义文件中一个区块的相关信息。标题组,多层次的标题。它将一组
~
元素分组
-
标记定义一组媒体内容以及它们的标题
-
标记定义 figure 元素的标题
-
标记定义一个对话框(会话框)类似微信
多媒体标签
- video(视频)
- audio (音频)
- source:媒介元素(音视频)
- embed:嵌入插件(音视频)
canvas标签
WebGL, svg, canvas 区别
block, inline, inline-block
设计模式
设计模式的五大设计原则和三大类
- 单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立
- 开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。这是软件设计的终极目标。
- 里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。
- 接口独立原则:保持接口的单一独立,避免出现“胖接口”。这点目前在TS中运用到。
- 依赖倒置原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”
设计模式的三大类
- 创建型:工厂模式,抽象工厂模式,建造者模式,单例模式,原型模式
- 结构型:适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式
- 行为型:策略模式,模板方法模式,发布订阅模式,迭代器模式,职责链模式,命令模式,备忘录模式,状态模式,访问者模式,中介者模式,解释器模式。
发布订阅模式(publish-subscribe)
DOM0 和 DOM2级事件
区别:
-
语法的区别
- box.onclick = function{}
- Box.addEventListener('click', function(){})
-
DOM2 可以给一些特殊的事件类型绑定方法,这些不可以使用DOM0不支持,比如
- DOMContentLoaded, DOM树加载完成后执行...
- transitionend, 当CSS3的transition动画运行结束之后执行...
-
底层运行机制的区别
- DOM0 就是给元素的某个属性绑定方法(有效绑定的方法只有一个)
- DOM2 是基于事件池机制完成,每增加一个绑定的方法,都会向事件池中存放一个...,当事件触发会依次执行事件池中的事。
发布订阅就是模拟的事件池机制
JQuery中的发布订阅
$.Callbacks() 回调函数集合
add, remove
手写
和观察者模式的区别
其次,就是实现二者所需的角色数量有着明显的区别。观察者模式本身只需要2个角色便可成型,即观察者和被观察者,其中被观察者是重点。而发布订阅需要至少3个角色来组成,包括发布者、订阅者和发布订阅中心,其中发布订阅中心是重点。
我的理解是:发布订阅模式就是观察者模式解耦之后的产物。在观察者模式中,被观察对象除了自身的功能之外,还需要维护一个观察者列表。如果把这部分观察者列表抽取出来成为单独的一部分就成了发布订阅模式中的发布订阅中心。
算法与数据结构
前序(后序)中序推导树
最长回文子串。
不可能的出栈顺序
十大经典排序算法总结(JS)
反转链表
KMP算法 模式匹配算法
GCD算法 寻找最大公因数
leetcode 1071
```
const gcd = (a, b) => (0 === b ? a : gcd(b, a % b))
```
二分法 二分查找
堆
完全二叉树,根结点是三者中最大(小)的值。实际操作中使用数组来表示。位置关系为:根结点x,左子节点2x+1,右子节点2x+2.
插入的时候不管是左还是右,父节点位置都是他们的(cur - 1) >> 1
有两种调整方式(加新节点的方法)
插入调整:新元素直接加到数组末尾,然后自下向上依次调整
弹出调整:弹出堆顶元素,数组的最后一个元素(通常就是新元素)代替成为堆顶,然后自上向下依次调整
回溯法
组合问题
如果限定长度(k)。就可以用i <= n - (k - path.length) + 1剪枝。比如77、216
如果限定的是和(target),就可以剪枝已经大于target的。即 && sum + nums[i] <= target。 比如39
是否需要startIndex:对于组合问题,在同一个集合里求就需要,每次选取都从不同集合中则不需要。比如17. 电话号码的字母组合
切割问题(组合问题的变种) 131.分割回文串,93.复原IP地址。重点在于判断是否合法的工具函数的写法。
有重复元素,却不能有重复组合。也就是回溯树中层级的剪枝。
可以先对数组排序,然后增加条件 i == startIndex || nums[i] == nums[i - 1]. 即不重复才能继续递归。比如 40.组合总和II,90.子集II
但有时,源集合不能排序。比如 491.递增子序列。则必须使用used集合去重。
由于是层级剪枝(去重),used初始化与for循环前,如果存在跳过,不存在则继续迭代并添加当前值进used。
排列问题
因为可以向前选数字,所以没有startIndex,但是这样就不能排除已经选进path里的。所以需要借助used去重。但是和上面的层级去重不一样,这里是支级去重。所以used和res一起初始化.
有时会出现同时需要层级和支级剪枝(去重)的情况,比如 47.全排列 II
此时需要同时使用used(支级)去重法 和 排序,比较nums[i] ?== nums[i - 1]去重法(层级)。47题需要特别注意,由于同时使用两种去重法,if的条件就是i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue 多了一个 !used[i - 1]因为如果前一个已经在支级被去重了,自然不会再在层级出现重复。
N皇后
在棋盘中判断两点是否是处于一条斜线: row1 + col1 == row2 + col2 || row1 - col1 == row2 - col2
集合划分问题
多个桶,每个值可以选择任意一个桶。
var canPartitionKSubsets = function(nums, k) {
//递减排序的目的是为了让剪枝 1 的命中率更高。
//即选数字的时候,先用大的挤占桶空间
nums.sort((a, b) => b - a);
const sum = nums.reduce((a, b) => a + b);
if((sum % k) !== 0){
return false;
}
const target = sum / k;
//桶,也就是每个集合的sum
const bucket = new Array(k).fill(0);
const backtracing = (nums, startIndex, bucket, k, target) => {
if(startIndex == nums.length){
return true;
}
for(let i = 0; i < k; i++){
//剪枝2
if(i > 0 && bucket[i] == bucket[i - 1]){
continue;
}
//剪枝1
if(bucket[i] + nums[startIndex] > target){
continue;
}
bucket[i] += nums[startIndex];
if(backtracing(nums, startIndex + 1, bucket, k, target)){
return true;
}
bucket[i] -= nums[startIndex];
}
return false;
}
return backtracing(nums, 0, bucket, k, target);
};
背包问题
01背包(二维)
//weight,value 数组。分别代表物品的重量和价值
//size。背包的大小
function bag01(weight, value, size) {
const length = weight.length;
const dp = new Array(length).fill().map(()=>new Array(size).fill(0));
for(let j = weight[0]; j <= size; j++){
dp[0][j] = value[0];
}
for(let i = 1; i < length; i++){
for(let j = 0; j < size + 1; j++){
if(j < weight[i]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
console.table(dp);
return dp[length - 1][size];
}
console.log(bag01([1, 3, 4], [15, 20, 30], 4));
01背包(一维)
function bag01 (wight, value, size){
const length = wight.length;
const dp = new Array(size + 1).fill(0);
for(let i = 0; i < length; i++){
for(let j = size; j >= wight[i - 1]; j--){
dp[j] = Math.max(dp[j], dp[j - wight[i - 1]] + value[i - 1]);
}
}
return dp[size];
}
console.log(bag01([1, 3, 4], [15, 20, 30], 4));
除了普通的背包大小,物品价值、重量的题型。还有一种物品价值等于物品重量,要求特定背包大小能装多少or最多装多少?
416 分割等和子集 1049 最后一块石头的重量 II
对于这两道题,size为target,递推公式变为dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
完全背包
每个物品可以无限选择. 所以背包遍历时应该从小到大。
求组合
外层物品,里层背包。这样每个物品只会出现一次。
518 零钱兑换 II
求排列
外层背包,里层物品。注意此时需要额外添加if(j >= nums[i])因为此时背包是从0开始遍历的
377 组合总和 Ⅳ(只是叫组合总和,实际上是排列组合)
多重背包
每件物品有限定 Mi 的使用次数。可以把 多个同种物品平铺开变成 Mi 个该物品。这样就转换成了01背包问题
const multiPack = (bagSize, weightArr, valueArr, amountArr) => {
//展开多个同种物品
for(let i = 0; i < amountArr.length; i++){
while(amountArr[i] > 1){
weightArr.push(weightArr[i]);
valueArr.push(valueArr[i]);
amountArr[i]--;
}
}
//展开后的,实际的可选的物品总数
//此时就是一个01背包问题
const itemNum = weightArr.length;
const dp = new Array(bagSize + 1).fill(0);
for(let i = 0; i < itemNum; i++){
for(let j = bagSize; j >= weightArr[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weightArr[i]] + valueArr[i]);
}
}
return dp;
}
console.log(multiPack(10, [1, 3, 4], [15, 20, 30], [2, 3, 2]));
其它动态规划
打家劫舍
const rob = nums => {
// 数组长度
const len = nums.length;
// dp数组初始化
const dp = [nums[0], Math.max(nums[0], nums[1])];
// 从下标2开始遍历
for (let i = 2; i < len; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
};
股票问题
- 全程只能买卖一次。
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
- 全程可以买卖多次(同一时间只能持有一只股票)
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
- 全程可以买卖两次 (5种状态)
dp[i][0] = dp[i - 1][0]; //没有操作
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); //第一次买入
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]); //第一次卖出
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]); //第二次买入
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]); //第二次卖出
- 全程可以买卖 k 次 (2 * k + 1 种状态)
//0无操作 //奇数表示买入 //偶数代表卖出
for(let j = 1; j < k * 2; j += 2){
dp[0][j] = -prices[0];
}//统一初始化,第一天第n次买入,都为-prices[0]
for(let i = 1; i < len; i++){
dp[i][0] = dp[i - 1][0];
for(let j = 0; j <= (k - 1) * 2; j += 2){
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
}
- 无限次交易但包含冷冻期 (4个状态)
dp[0][0] = -prices[0];
//其它状态第一天都为0
for(let i = 1; i < prices.length; i++){
//此时持有股票.
//分别代表,前一天就已经持有;前一天不持有且不在冷冻期所以今天买入;上上天刚卖,所以前一天在冷冻期,今天冷冻期已过并买入
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i], dp[i - 1][3] - prices[i])
//此时不持有股票因为今天刚卖
dp[i][1] = dp[i - 1][0] + prices[i];
//此时不持有股票,且不在冷冻期
//分别代表,前一天也不持有且不在冷冻期;前一天是冷冻期
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][3]);
//此时是冷冻期,所以前一天肯定是卖出的那天
dp[i][3] = dp[i - 1][1];
}
//最后返回状态1、2、3最后一天的最大值
- 包含手续费,easy不写
前端面试手写代码
前面已经包括过的
Promise及其API
Promise.retry
原生AJAX封装
防抖、节流
数组去重
发布订阅模式
数组乱序
//方法1,伪随机。
//v8在处理sort方法时,使用了插入排序和快排两种方案。当目标数组长度小于10时,使用插入排序;反之,使用快速排序。
//sort进行乱序,理想的方案或者说纯乱序的方案是数组中每两个元素都要进行比较。这样一来,总共比较次数一定为n(n-1)。
//而在sort排序算法中,大多数情况都不会满足这样的条件。因而当然不是完全随机的结果了。
const shuffle = (arr) => {
return arr.sort(() => {
return Math.random() - 0.5;
})
}
//方法2 费雪耶茨洗牌算法 真随机
const shuffle = (arr) => {
let m = arr.length;
while (m > 1){
let index = Math.floor(Math.random() * m--);
[arr[m], arr[index]] = [arr[index], arr[m]]
}
return arr;
}
setTimeout和setInterval互相实现
//setInterval实现setTimeout
const mySetTimeout = (callback, time) => {
const timer = setInterval(()=>{
clearInterval(timer);
callback();
}, time);
}
//setTimeout实现setInterval
const mySetInterval = (callback, delay) => {
function recursion() {
callback();
setTimeout(recursion, delay);
}
setTimeout(recursion, delay);
}
柯里化函数
对象扁平化,及相反
//对象扁平化
const flatObj = (obj, preKey='', res = {}) => {
for(const key in obj) {
//hasOwnProperty方法排除原型链上的属性
if(obj.hasOwnProperty(key)) {
const newKey = `${preKey}${key}`
if(typeof obj[key] == 'object'){
//还有嵌套的对象,继续迭代
flatObj(obj[key], `${newKey}`, res)
} else {
//迭代到底,填充答案
res[newKey] = obj[key]
}
}
}
return res;
}
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
flatObj(source) //{abc: 1, abd: 2, ae: 3, fg: 2}
数组扁平化,及相反
//数组扁平化
//arr.flat数组偏平化,参数“Infinity”是偏平化深度
const arrFlat = (arr) => {
return [...new Set(arr.flat(Infinity))].sort((a, b) => a - b);
}
//转JSON字符串,删除除头尾以外的所有'['']'之后。重新转为数组(待验证)
//手写递归
Array.prototype.flat = function() {
const result = this.map(item => {
if(Array.isArray(item)) {
return item.flat()
}
return [item]
});
return [].concat(...result)
}
模拟块级作用域
//闭包模拟
for (var i=0; i<10; i++) {
function fn(j) {
return function() {
setTimeout(_ => {
console.log(j)
}, j*1000)
}
}
fn(i)();
}
//立即执行函数模拟
for (var i=0; i<10; i++) {
(function(j) {
setTimeout(_ => {
console.log(j)
}, j*1000)
})(i)
}
//能理解的一版
for(var i = 0; i < 5; i++){
(function(n){
console.log(n);
})(i)
}
使(a == 1) && (a == 2) && (a == 3)为真
//引用类型在和基本数据类型做比较时,会隐式调用toString()方法,将其转换为基本数据类型。所以本题重写toString()方法即可
//数组解法
let a = [1,2,3];
a.toString = a.shift;
console.log(a == 1 && a == 2 && a == 3); //true
//对象解法
let a = {
num:1,
toString:function () {
return this.num++
}
}
console.log(a == 1 && a == 2 && a == 3) //true
大数相加
compose函数和pipe函数
组合(compose)和管道(pipe),一个从后往前迭代,一个从前往后迭代。
函数组合是指将多个函数按顺序执行,前一个函数的返回值作为下一个函数的参数,最终返回结果。 这样做的好处是可以将复杂任务分割成多个子任务,然后通过组合函数再组合成复杂任务。我们要实现的就是这样的组合函数(compose 函数),实际就一个工具函数。
compose函数的应用: Webpack中loader的加载顺序 也是从右向左,通过compose实现的
//编译less文件 less => css => 打包到JS中 => 插入到dom中
['style-loader', 'css-loader', 'less-loader']
const contentStr = compose(...loaders)(源文件)
function fn1(num) {
return num + 1
}
function fn2(num) {
return num + 2
}
function fn3(num) {
return num + 3
}
// 我们需要实现这样一个组合函数,接受参数为需要组合的子函数,最后返回组合后的函数。
// 最终目的使 compose(fn3,fn2,fn1)(5) === fn3(fn2(fn1(5)))
//普通方法
function compose(...fns) {
// reverse() 作用是使输入函数从右往左执行
fns = fns.reverse()
return function(...args) {
let res;
fns.forEach((fn,index) => {
if(index === 0) {
res = fn.apply(this, args)
} else {
res = fn(res)
}
})
return res
}
}
//reduceRight (reduce)
const compose = (...funs) => {
return function(...params){
return funs.reduceRight((prevRes, curFun) => curFun(prevRes), ...params);
}
}
函数柯里化
柯里化应用1 浏览器兼容性检测:判断浏览器的事件监听。
addEventListener 其它大部分
attachEvent IE浏览器
const whichEvent = (function(){ //立即执行函数,文件的头部直接判断
if(window.addEventListener){
return function(element, type, listener, useCapture){
element.addEventListener(type, function(e){
listener.call(element, e);
}, useCapture);
}
}else if(window.attachEvent){
return function(element, type, handler){
element.attachEvent('on'+type, function(e){
handler.call(element, e);
})
}
}
})();
柯里化应用2 延迟执行
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4) = 10;
手写数组map方法
call, apply, bind方法
//call
Function.prototype.newCall = function(context, ...args){
context = context || window; //防一手context = null
//给context这个对象加一个Symbol属性
const fn = Symbol();
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
//apply, 和call的唯一区别就是args不用拓展运算符因为接收的参数本来就是数组
Function.prototype.newApply = function(context, args){
context = context || window; //防一手context = null
//给context这个对象加一个Symbol属性
const fn = Symbol();
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
//bind
Function.prototype.newBind = function (context, ...args) {
let that = this;
return function () {
return that.call(context, ...args)
}
}
function person(a, b){
return{
name: this.name,
a: a,
b: b
}
}
let sxy = {name: 'sunxiangyu'}
let result = person.newCall(sxy, 23, 'male');
console.log(result);
LRU
class ListNode{
constructor(key, value){
this.key = key;
this.value = value;
this.pre = null;
this.next = null;
}
}
class LRUCache{
constructor(capacity){
this.capacity = capacity;
this.map = {};
this.count = 0;
this.dummyHead = new ListNode();
this.dummyTail = new ListNode();
this.dummyHead.next = this.dummyTail;
this.dummyTail.pre = this.dummyHead;
}
get(key){
let node = this.map[key];
if(!node){
return -1;
}
this.moveToHead(node);
return node.value;
}
put(key, value){
let node = this.map[key];
//没有则增添
if(!node){
//超过容量,删掉个LRU
if(this.count === this.capacity){
this.removeLRU();
this.count--;
}
let newNode = new ListNode(key, value);
this.map[key] = newNode;
this.addToHead(newNode);
this.count++;
}else{
//有则更新
node.value = value;
this.moveToHead(node);
}
}
moveToHead(node){
this.removeNode(node);
this.addToHead(node);
}
removeNode(node){
[node.pre.next, node.next.pre] = [node.next, node.pre];
}
addToHead(node){
let head = this.dummyHead.next;
this.dummyHead.next = node;
head.pre = node;
node.pre = this.dummyHead;
node.next = head;
}
removeLRU(){
let tail = this.dummyTail.pre;
this.removeNode(tail);
delete this.map[tail.key];
}
}
Promise实现并发器(三种题型)
不使用API查找某个DOM节点下所有含有某个class的DOM节点
其它
前端工程化
https://mp.weixin.qq.com/s/d8FixSm6EYDwD-IaUaCztQ
前端常用性能优化
图片懒加载
在服务器数据回来之前,会有一个默认的图
有相应的插件 vue-lazyload
引入后使用自定义指令 v-lazy 替换 :src
雪碧图
把网站上会用到的小图标整合到一张单独的图片中。从而减少http请求的数量。然后再需要用到图片的地方使用background-image和background-position来将需要的部分显示在合适的位置(现在好像都被iconfont代替了)
Iconfont
Iconfont 指用字体文件取代图片文件,来展示图标、特殊字体等元素的一种方法。
路由懒加载
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
词法分析、语法分析、语义分析
文件物理结构及其特点