JavaScript 基础总结
数据类型
- 基本类型
- number
- boolean
- string
- null
- undefined
- symbol (ES6)
- bigint (ES2020)
- 引用类型
- Object 及其所有派生类
number
number 类型遵守 IEEE754 标准,使用 64 位表示一个数字,任意数字 N 可以表示为:
N=Re.MN = R^e.M
- R:比例因子的基数,一般规定为 2, 8, 16 等常数
- e:比例因子的指数
- M:尾数,为一个纯小数
分别占用:
- 符号位:1 bit
- 指数位:11 bit
- 小数位:53 bit
最大安全数字为:
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
详细参见文章:IEEE 754格式是什么? - wuxinliulei的回答 - 知乎
symbol
利用其唯一性可以用来表示一个唯一的对象引用,只能通过传递变量的方式获取到相同的 Symbol 实例。
可以用于模拟私有变量,将 symbol 作为成员索引,可以使对象的成员无法被外部代码索引获取。
NaN
Not a Number,即无法用 number 类型表达的数据,但是判断类型时有 typeof NaN === 'number'
。
类型转换
进行非基础类型的转换时会先调用 valueOf,如果返回的不是基本类型的值,那么会转而调用 toString。
{} + [] === '[object Object]' + '' === '[object Object]'
严格相等
- 宽松相等
==
:会自动进行强制类型转换 - 严格相等
===
:禁用强制类型转换
对象克隆
浅克隆,将对象的成员全部复制到新的对象上,但如果成员中存在嵌套的对象,这个成员对象在复制前后会保留相同的引用。
function shallowClone(obj) {
let cloneObj = {}
for (let i in obj) {
cloneObj[i] = obj[i]
}
return cloneObj
}
深克隆,所有深度嵌套的基础类型成员将被拷贝,所有对象的构造函数都会改为指向 Object,注意 RegExp、Date、函数等对象不是 JSON 安全的。
function deepCopy(obj) {
if (typeof obj === 'object') {
var result = obj.constructor === Array ? [] : {};
for (var i in obj) {
result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
}
} else {
const result = obj
}
return result
}
构造函数
使用 new 关键字可以将函数作为构造函数来初始化获得对象:
const set = new Set()
构造流程:
- 创建一个新的对象 set
- 对象的隐藏属性
[[Prototype]]
链接到构造函数 Set.prototype - 新对象绑定函数调用的 this 上下文
- new 自动返回新对象,或函数返回其它对象
对象数组
由于索引机制的原因,对象可以和数组一样通过数字进行索引,使用 Array.from
能够将其转换为数组。函数的 arguments 对象就是这样的类数组。
语法
作用域
- 全局作用域:声明在最外层或是没有使用任何关键字声明的变量都是全局变量
- 局部作用域:又称函数作用域,使用
var
关键字声明在函数块内部的变量是局部变量 - 块级作用域 (ES6):使用
let
const
关键字声明在花括号{}
内部的变量都处在块级作用域
另外还有两种作用域的可见性规则:
- 静态作用域:又称词法作用域,在代码定义时确定,在函数定义的位置逐层向外寻找变量
- 动态作用域:在代码运行时确定,在函数执行的位置逐层向外寻找变量,JS 中仅 this 变量使用
更多内容参考:
- zh.wikipedia.org/wiki/%E4%BD…
- juejin.cn/post/684490…
- www.zhihu.com/question/20…
作用域链
形成词法作用域时,每个作用域中都存在一个词法环境对象,可以对它进行定义:
/** 词法环境 */
interface LexicalEnvironment {
/** 环境记录 */
record: Record<string, any>
/** 外层词法环境的引用 */
outer: LexicalEnvironment
}
在作用域内声明变量可以看作是在声明环境记录的成员,
let phrase = 'Hello'
function say(name) {
console.log(phrase, name)
}
say('John')
如果手动构建对象来表示这段代码的作用域:
const globalEnv = {
record: {
phrase: 'Hello'
},
outer: null
}
const fnEnv = {
record: {
name: 'John'
},
outer: globalEnv
}
在 say
函数中读取 name
时就会访问 fnEnv.record.name
,而读取 phrase
时首先尝试在 fnEnv.record
中搜索,发现不存在后再到外层作用域获取 globalEnv.record.phrase
。访问任何词法变量都将按照这个规则,逐层向外寻找变量直到到达顶层。
因为作用域彼此之间会形成链状的引用关系,我们就将这样的结构成为作用域链。
“词法环境”是一个规范对象(specification object):它仅仅是存在于 编程语言规范 中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
- zh.javascript.info/closure
- segmentfault.com/a/119000001…
闭包
- 产生闭包:将引用了上级块作用域的函数作为值传递出作用域范围
- 应用场景:
- 柯里化
- 模块
- 箭头函数使用 this
- 处理事件
this 上下文
this 的指向通过下面四种方式绑定,越靠下的优先级越高。
- 默认绑定:没有修饰的情况
- 非严格模式指向全局对象
- 严格模式指向 undefined
- 隐式绑定:函数作为对象的成员被调用,或依照动态作用域规则在调用位置寻找 this
- 显式绑定:对函数上执行 call、apply、bind 操作,实现上可看作隐式绑定的延伸
- new 绑定:成员函数将被绑定到新构建的对象上
例如对构造函数进行显式绑定是无效的,因为 new 操作符的优先级更高。
EventLoop
每轮事件循环中都会先执行宏任务队列,再执行微任务队列,完成后进入下轮循环。
- 宏任务:
- script
- setTimeout()
- setInterval()
- setImmediate()
- I/O
- UI Rendering
- 微任务:
- process.nextTick()
- Promise
setTimeout
执行 setTimeout(callback, delay)
后会在 delay
毫秒的延迟后将 callback
放入队列,等待函数调用栈清空时才会执行。
Promise
有时面试会要求手写可以进行 then 调用的 Promise,这里只要知道几个要点:
- 初始化 Promise 传入的 executor 会被即刻执行
- executor 内部执行 resolve 或 reject 会清算 callbacks
- 调用
.then
会注册 callback,如果已经清算则直接执行 callback
type Executor<R> = (resolve: (result: R) => void, reject: (reason?: any) => void) => any
class SimulatedPromise<R> {
resolvedCallbacks: {(result: R): void}[] = []
rejectedCallbacks: {(reason?: any): void}[] = []
state: 'PENDING' | 'RESOLVED' | 'REJECTED' = 'PENDING'
value: any
constructor(executor: Executor<R>) {
const resolve = (result: R): void => {
if (this.state === 'PENDING') {
this.state = 'RESOLVED'
this.value = result
this.resolvedCallbacks.forEach(cb => cb(result))
}
}
const reject = (reason?: any) => {
if (this.state === 'PENDING') {
this.state = 'REJECTED'
this.value = reason
this.rejectedCallbacks.forEach(cb => cb(reason))
}
}
executor(resolve, reject)
}
then(onFulfilled: (result: R) => void, onRejected?: (reason?: any) => void) {
if (this.state === 'PENDING') {
this.resolvedCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
}
if (this.state === 'RESOLVED') {
onFulfilled(this.value)
}
if (this.state === 'REJECTED') {
onRejected(this.value)
}
}
}
const promise = new SimulatedPromise<number>(resolve => {
setTimeout(() => resolve(1), 1000)
}).then(value => {
console.log('Rejected: ', value)
})
DOM
DOM 事件
基于发布订阅模式,DOM 事件标准:
- DOM L0:通过 onclick 或是 dom.onclick 等节点属性定义事件,每个元素同一事件只能被绑定一次
- DOM L2:通过 addEventListener, removeEventListener 注册和删除事件,每个事件都能注册多次
- DOM L3:增加了更多事件类型
事件流
事件流是网页 DOM L2 标准的事件接收顺序,其包含三个阶段:
- 事件捕获阶段:首先发生并为捕获事件提供机会
- 目标接受事件:实际的目标获得事件
- 事件冒泡阶段:从底层向上传播,对事件做出响应
查看 CodePen 上的示例
跨源 CORS
为了保证不同域名的网站间能够进行通信和资源共享,并且保证这个过程的安全,浏览器要求跨站请求时要遵守 CORS 的相关规则。跨源请求的整个过程是这样的:
预检请求
使用预检请求可以避免跨域请求对服务器的用户数据产生未预期的影响。进行跨域请求前浏览器会先进行判定,满足一定条件的简单请求1可以跳过这个阶段直接发起请求。但是不管是否需要预检,进行跨源请求时如果响应的 Header 字段不允许跨源,那么前端代码就接收不到这次响应。
如果在正式请求前需要预检,浏览器会先发送一个 OPTION 请求询问服务器的跨源规则:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
- Access-Control-Request-Method:告知实际请求将使用 POST 方法
- Access-Control-Request-Headers:告知实际请求将携带两个自定义请求 Header 字段
服务器收到预检请求后将据此决定实际请求是否被允许,并在 Header 中返回相关规则:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
- Access-Control-Allow-Methods:允许客户端使用 POST, GET 和 OPTIONS 方法发起跨源请求
- Access-Control-Allow-Headers:允许请求中携带字段 X-PINGOTHER 与 Content-Type
- Access-Control-Max-Age:表明该响应的有效时间为 86400 秒,有效时间内不需要为同一请求再次发起预检
与前述简单请求不同,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
更多内容可以参考 MDN 文档:跨源资源共享(CORS)
- developer.mozilla.org/zh-CN/docs/…↩
作者:掘金-JavaScript 基础总结