基础知识(二)
(一)Promise的理解
是 JavaScript 中异步编程的一种解决方案,代表一个尚未完成但承诺将来完成的操作。
它有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
早期使用回调函数处理异步,但容易出现回调地狱,是的代码可读性变得很差。
在实际开发中,Promise和async/await是配套使用的,async/await是基于Promise语法糖;底层本质还是Promise状态控制。
async/await:语法糖,返回的是Promise,内部用Generator+Promise封装控制流
(三)this的指向
var j = o.b.fn;
j();
o.b.fn();
这两种不一样,第一个的this指向window,第二个的指向b;
var obj = new test();
obj.x // 1
new绑定,指向创建的实例对象
如果构造函数返回对象,则this指向返回的对象;返回简单类型或null时指向创建的实例对象;
显式修改: c(参数列表) a(数组) b(返回绑定this之后的函数,不立即执行)
箭头函数:编译时绑定。所以不能作为构造函数,没有arguments(若要获取参数可以使用剩余参数语法...args),不能作为generator函数
优先级:new>c a b>隐式>默认
箭头函数的this是在词法作用域(即定义时)就确定的,而不是在运行时绑定的。而构造函数需要能够动态this绑定,当new一个新对象时,需要将这个新对象绑定为构造函数内部的this,而箭头函数的this是不可改变的(无法通过call apply bind修改)
(三)虚拟DOM
1、性能优化:每次操作真实DOM都会触发浏览器的回流和重绘,成本很高。
在内存中用JS构建一个虚拟节点树VNode;当状态变更时,生成一个新的Vnode树;做Diff比较;最后支队真正有变化的Dom节点打补丁。
2、跨平台能力:虚拟Dom是纯JS数据结构,不依赖浏览器Dom
Vue2:用户写的模板运行render函数得到Vnode模拟真实节点,每次数据变化重新执行render函数得到新的Vnode树,patch函数对比Vnode并更新真实Dom。
Vue3:使用Proxy+编译优化(template->render函数),静态提升+block tree进一步减少diff范围。
改进:vue2每次数据变更都要全组件重新render,没有对静态内容优化,每次render仍然会重新创建静态节点。vue3编译阶段优化静态提升,将不依赖响应式数据的静态节点抽离到组件外,只创建一次;在编译时把模板切分成一个个block,每个block只会追踪自己的依赖;diff算法优化,加入了patch flag标注那些部分是可能变化的,哪些是静态的。
(四)跨域
前后端分离:前后端部署在不同地址,前端的页面 URL 和后端 API 的地址 “不同源”,所以会引起跨域。
比较的双方是:
- 页面所在的源(即当前打开页面的地址,如
http://localhost:3000
) - 请求目标的源(如你请求的接口地址:
http://localhost:5000/api/user
)
跨域问题中的“谁和谁跨域”
跨域问题(Cross-Origin Resource Sharing, CORS)指的是 浏览器 在访问不同 源(Origin) 的资源时,由于 同源策略(Same-Origin Policy, SOP) 的限制,导致请求被阻止的现象。具体来说,跨域问题涉及以下两个主体:
主体 | 说明 |
---|---|
客户端(浏览器) | 发起请求的网页,运行在某个域名(如 https://www.example.com ) |
服务端(API/资源服务器) | 提供数据的后端服务,运行在另一个域名(如 https://api.example.com ) |
跨域的本质:
浏览器的 ****当前网页(客户端)向 不同源的服务端 发送请求时,浏览器会检查服务端是否允许跨域访问。如果服务端未正确配置 CORS,浏览器会阻止请求。
什么是“同源”
同源策略(Same-Origin Policy, SOP) 规定:
只有当协议(Protocol)、域名(Domain)、端口(Port)完全一致时,才不算跨域
跨域问题的表现
当浏览器检测到跨域请求时,会:
先发送
OPTIONS
预检请求(Preflight Request),询问服务端是否允许跨域。如果服务端未返回正确的 CORS 头
(如Access-Control-Allow-Origin),浏览器会报错:
Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'https://www.example.com' has been blocked by CORS policy. No 'Access-Control-Allow-Origin' header is present on the requested resource.
跨域问题的解决
1、jsonp: <script>
标签不受同源策略影响
2、cors:后端设置cross-origin-access
3、webpack-dev-server起一个本地服务器
4、nginx反向代理
(五)JS模块化
CommonJS:同步加载,阻塞执行,运行时解析
AMD:异步模块定义,异步加载,运行时解析
现在替代方案:ES6 import export,编译时优化,支持treeShaking,浏览器原生支持
(六)事件模型
原始事件模型(DOM0):onclick 绑定速度快,跨浏览器器优势,只支持冒泡,同一个类型的事件只能绑定一次。
标准事件模型(DOM2):addEventListener\removeEventListener,第三个参数useCapture设置为false在冒泡过程中运行,true在捕获阶段运行。
(七)数据类型判断
typeof:null -> object ,引用类型除了function其余都判断为Object
instanceof:可以判断引用数据类型,但不能判断基础数据类型
Object.prototype.toString: [Object type]
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}
(八)事件委托
利用事件冒泡,点击子元素冒泡到父元素
(九)Ajax
Ajax
的原理简单来说通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript
来操作DOM
而更新页面
(十)Vite
解决开发服务器过慢:在大型项目中使用webpack,改动某部分代码后,页面渲染新版本的时间过长,vite的冷启动和热更新都更快;虽然现在有原生的ESM,但是由于嵌套引入会需要额外的网络传输往返,vite使用rollup打包。
解决方式:
1、开发服务器启动过慢:vite将代码分为依赖和源码,依赖大多是js,开发过程中不会经常改变,使用ESbuild(Go语言写的,比基于js的打包工具更快)打包这些依赖会更快,源码通常需要转换JSX、Vue等非纯js的代码,并且需要经常被更改,vite使用ESM,实际上是让浏览器接管了打包工具的部分工作,vite只需要根据浏览器的请求按需转换和提供源码。
2、更新过慢
原本的当一个文件更新后,重新构建整个包效率会很低。vite的HMR通过原生ESM提供的。只需要准确地使更新的模块与最近的HMR边界之间的链路失效即可。
(十一)正则表达式
\d 数字 + 匹配前面一个表达式1次或多次 g 全局
(十二)事件循环
执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中,遇到宏任务放入宏任务队列
当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完,再执行宏任务
微任务:Promise.then,this.$nextTick;宏任务:外层同步代码,setTimeout,setInterval,setImmediate
(十三)BOM,DOM
BOM:浏览器对象模型,DOM:文档对象模型
(十四)垃圾回收机制(GC)
引用计数:相互引用时会出现内存泄漏
标记清除:它从根(即全局对象)开始,找到所有从根引用的对象,然后找到这些对象引用的所有对象,如此反复。如果一个对象无法通过这个算法到达,它将被垃圾回收。一个对象是否仍然可以从根到达,是决定该对象是否会被垃圾回收的关键
(十五)图片格式选择
1、点阵图:一点一点像素组合而成的图。JPG(有损压缩,会降低图片的质量,且不可逆),PNG(无所压缩,压缩后不影响图片的质量,不过会比JPG大,同时可以有透明背景),WebP(有损压缩后比JPG小,无损压缩比PNG小)
GIF:点阵的动画,WebM更小。
2、矢量图:数学公式绘制而成的图。SVG(图片放大时,不会失真)
像logo icon,使用svg
(十六)js本地存储
localstorage:大小5M或更大,存储持久数据,除非主动删除数据 存令牌token jwt
sessionStorage:大小5M或更大,当前浏览器窗口关闭后自动删除
cookie:大小不超过4K,设置的cookie过期时间之前一直有效,自动传递到服务器
indexDB
(十七)纯函数
无状态+数据不可变:
- 函数内部传入指定的值,就会返回唯一确定的值
- 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数
(十八)柯里化
使用闭包实现柯里化,把多参函数转成嵌套的一元函数
(十九)节流 防抖
1、节流:高频触发时,同间隔执行多次,按固定时间间隔触发
比如监听滚动事件:举例来说,要判断使用者是否已经滑动到页面的 30% 处,当到达时会触发一些动画效果,因此,会透过监听滚动事件时计算是否已到达该位置,但如果只要一滚动就计算会非常消耗性能,透过节流(throttle) 可以将计算的这个回调函式在固定时间内合并一次被执行。
这里不适合使用防抖的原因是,防抖只会在事件停止被触发后的一段时间内被执行一次。因此如果用防抖,当使用者一直滑动页面,函式就永远不会被触发。
function throttle(fn,delay= = 500){
let timer = null;
return (...args) =>{
if(timer) return;
timer = setTimeout(() =>{
fn(...args);
timer = null;
},delay);
}
}
或者使用时间戳写法,判断当前时间和上一次执行的时间的差,如果大于阈值则执行,否则不执行
2、防抖:高频触发时,只执行最后一次,以最后一次触发为计时起点
比如搜索框
function debounce(fn,delay = 500){
let timer;
return (...args) =>{
clearTimeout(timer);
timer = setTimeout(() =>{
fn(...args);
},delay);
}
}
(二十)元素是否进入可视化区域
el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
元素距离顶部的距离-滚动距离 <= 可视化区域高度
intersection observer
(二十一)大文件上传
分片上传、断点续传
(二十二)触底刷新
scrollTop + clientHeight >= scrollHeight
滚动距离+可视化区域高度 >= body所有元素的总长度
下拉刷新:监听原生touchstart touchmove和 touchend
(二十三)单点登录
同域名下的单点登录:将cookie的domain属性设置为当前域的父域,将token保存在父域中。
不同域名下的:
1、session+cookie:需要有个认证中心,用户使用账号密码进行登录,认证中心有个session表(键值对),利用cookie把sid带给浏览器,浏览器访问子系统时携带sid,子系统会将接收到的sid发给认证中心,查到后会告诉子系统该用户完成登录了。
1、单token:认证中心生成token给浏览器,访问子系统时携带token,子系统自行认证token的可用性。
2、refreshToken:原token过期时间很短,如果原token失效,用户使用refreshToken发给认证中心去验证,认证中心返回一个新的token,再去访问子系统
(二十四)性能优化指标
LCP:最大内容绘制,页面中最大内容元素渲染完成的时间
FID:首次输入延迟,用户首次与页面交互到浏览器实际响应的时间
CLS:累计布局偏移,页面声明周期内所有意外布局偏移的总分数
(二十五)异步编程 三个请求依次返回
1、回调地狱
2、async await
3、promise.all
4、promise链式调用
5、for of 结合async await
(二十六)原型链
每个对象(Object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上知道一个对象的原型为null。根据定义,null没有原型,并作为这个原型链(prototype chain)中的最后一个环节。
prototype和_proto_
先介绍两个名词prototype和_proto_,举一个简单的例子更为直观。定义一个函数Foo,而后创建一个Foo的实例对象o1;
function Foo(){}
const o1 = new Foo()
1、原型对象-prototype
所有的函数都是对象,拥有独立属性prorotype
该属性指向一个对象,当函数(如:Foo
)被实例化成一个对象(如:o1
)后,实例对象(o1
)可以访问到函数(Foo
)的原型对象(prototype
)
如:在Foo
的prototype
上增加属性propA
,其值为'p1'
,可以发现在o1
上也可以获取到属性propA
,其值同样为'p1'
Foo.prototype.propA = 'p1'
o1.propA // 'p1'
那么o1
是如何获取到Foo.prototype
上的方法的呢,这就要介绍另一个概念,隐式原型__proto__
2、隐式原型-_proto_
所有非内置对象都是函数的实例,拥有独立属性_proto_
__proto__隐式原型是对象的独有属性。指向其构造函数的原型对象。
对象(如:o1
)的__proto__
属性指向其构造函数(如:Foo
)的原型对象( prototype
)
,对象o1
的构造函数为Foo
所以:o1能访问到属性propA
o1.propA === o1.__proto__.propA // true
o1.__proto__.propA === Foo.prototype.propA // true
原型链图示
1、实例化关系
o1.constructor === Foo // true
Foo.constructor === Function // true
Function.constructor === Function // true
可以得到:
o1
的数据类型是对象,是函数Foo
的实例化Foo
的数据类型既是函数也是对象,其是Function
的实例化Function
既是函数也是对象,其是自身Function
的实例化
2、独有属性分析
在实例化关系的基础上,继续分析每一级的属性关系。其中绿色代表函数独有属性,红色代表对象独有属性。
o1
的数据类型为对象,拥有独有属性__proto__
,由于prototype
是函数独有属性,所以o1
上的prototype
为undefined
Foo
的数据类型既是函数也是对象,所以其同时拥有属性prototype
和__proto__
Function
的数据类型既是函数也是对象,所以其同时拥有属性prototype
和__proto__
3、隐式原型引用关系
对象的隐式原型(_proto_)属性指向其构造函数(constructor)的原型对象(prototype)
o1.__proto__
指向其构造函数Foo
的原型对象(prototype
)Foo.__proto__
的指向其构造函数Function
的原型对象(prototype
)- 由于
Function
的构造函数是其自身,所以Function.__proto__
指向其自身的原型对象(prototype
)
由于函数的原型对象( prototype
)属性的数据类型为对象,因此同样具有对象的独有属性__proto__
默认情况下,对象隐式原型( __proto__
)指向其构造函数的原型对象( prototype
),那么Foo.prototype
和Function.prototype
的构造函数有指向哪里呢?
所有函数的原型对象( prototype
)的构造函数均指向其自身
可通过一下测试代码进行验证
Foo.prototype.constructor === Foo // true
Function.prototype.constructor === Function // true
内置函数对象的原型对象( prototype
),其隐式原型( __proto__
)也指向其自身
RegExp.prototype.constructor === RegExp // true
Date.prototype.constructor === Date // true
Map.prototype.constructor === Map // true
然而一些内置对象由于其没有函数特征,所以其原型对象( prototype
)属性为undefined
,其自身的constructor
指向Object
。
Math.prototype // undefined
Math.constructor === Object // true
言归正传,由于所有函数的原型对象( prototype
)的构造函数均为其自身,则如若Foo.prototype.__proto__
指向其构造函数的prototype
,即Foo.prototype.__proto__
指向Foo.prototype
。那么原型链的查找将进入无限循环。为了避免这个问题,则将所有函数原型对象( prototype
)的隐式原型(__proto__
)均指向Object.prototype
可通过以下代码进行验证:
Foo.prototype.__proto__ === Object.prototype // true
Function.prototype.__proto__ === Object.prototype // true
这里欠缺的有两个点
Object
既是函数也是对象,所以其拥有对象的独有属性(__proto__
),那么其隐式原型(__proto__
)指向哪里Object.prototype
的数据类型为一个对象,如果其隐式原型(__proto__
)仍指向Object.prototype
,那么原型链的查找将进入无限循环,那么其指向哪里
针对第1点,Object
自身既是函数又是对象,其作为对象的独有属性隐式原型( __proto__
)应指向其构造函数的原型对象( prototype
)。Object
对象的构造函数为Function
,所以其隐式原型( __proto__
)指向Function.prototype
可通过以下测试代码进行验证
Object.constructor === Function // true
Object.__proto__ === Function.prototype // true
因此其指向关系如下图:
针对第2点,起始什么是原型链已经给出了定义。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null
因此其最终指向为null
总结
对象通过隐式原型( __proto__
)属性指向其构造函数的原型对象( prototype
),进而通过原型对象( prototype
)的隐式原型( __proto__
)属性指向更高层级的原型对象( prototype
),最终指向null
而停止所形成的链条,则称其为原型链。
(二十七)typeof instanceof
typeof
获取一个对象的原始类型。
null -> object (js的历史bug)
引用数据类型除了function其余的均判断为object
typeof [] === 'object'
typeof {} === 'object'
instanceof
检查某个对象是否是某个构造函数的实例(即该对象的原型链上是否存在该构造函数的prototype)
原理:
function myInstanceof(obj,constructor){
let proto = Object.getPrototypeOf(obj); //等价于obj.__proto__
while(proto){
if(proto === constructor.prototype){
return true;
}
proto = Object.getPrototypeof(proto);
}
return false;
}
取出对象的 __proto__
(等价于 Object.getPrototypeOf(obj)
);
逐层向上遍历它的原型链;
每一层都和 constructor.prototype
比较;
找到匹配则返回 true
,否则最终返回 false
。
Object.prototype.toString.call()
js中最精准的数据类型判断方法。
Object.protoype.toString
默认情况下(直接调用),返回 "[object Type]"
,其中 Type
是对象的 内部 [[Class]] 属性(表示对象的类型)。
直接调用 toString()
时,方法内部的 this
指向调用它的对象。通过 call()
,可以强制将 this
绑定到任意值(包括基础类型),从而获取该值的类型标签。
为什么它能区分所有类型?
- JavaScript 引擎为每个值维护一个内部属性
[[Class]]
,表示值的类型。 Object.prototype.toString
通过访问[[Class]]
返回对应的标签。注意:ES6 后[[Class]]
被Symbol.toStringTag
替代- 当传入基础类型(如
42
、"hello"
)时,JavaScript 会临时将其包装为对应的对象(如new Number(42)
),然后访问其[[Class]]
。
总结
typeof
用来判断基础数据类型,引用数据类型除了function
其余均判断为object
;
instanceof
用来判断引用数据类型,判断不了基础数据类型(因为基础数据类型没有原型链);
通用数据类型判断方法:Object.prototype.toString.call()
console.log(Object.prototype.toString.call("hello") === "[object String]"); // true
console.log(Object.prototype.toString.call(42) === "[object Number]"); // true
为什么 "hello" instanceof String
返回 false
,而 new String("hello") instanceof String
返回 true
?
"hello"
是基础类型字符串,没有原型链,instanceof
无法检查。new String("hello")
是String
包装对象,属于引用类型,其__proto__
指向String.prototype
,因此instanceof
有效。
Object.prototype.toString.call()
它是未被重写的“原始版本”。
Object.prototype.toString
是 JavaScript 中所有对象 toString()
方法的“原始实现”,其他对象的 toString()
都是基于它重写的。它的核心作用是 返回对象的类型标签,而非字符串转换。
普通 toString()
:用于字符串转换(隐式调用或显式转换)。
为什么普通 toString()
不返回 [[Class]]
?因为大多数对象的 toString()
被重写为更实用的字符串转换逻辑。
ES6 的 Symbol.toStringTag
:
在 ES6 中,可以通过 Symbol.toStringTag
自定义 Object.prototype.toString
的返回值:
const obj = {
[Symbol.toStringTag]: "MyType"
};
console.log(Object.prototype.toString.call(obj)); // "[object MyType]"