inchill / fe-notes Goto Github PK
View Code? Open in Web Editor NEW🍎个人前端知识汇总,并且处于持续更新中。 This is a summary of personal front-end knowledges, and it will be updated from time to time.
🍎个人前端知识汇总,并且处于持续更新中。 This is a summary of personal front-end knowledges, and it will be updated from time to time.
DOM 元素在渲染时不会占用空间。将其从文档流中取出,并将其从可访问性树中删除,不会响应任何事件。
更改元素透明度,仍然在文档流中,并且可响应事件。假如页面上有几个单/复选框,可通过 tab 键顺序切换。
更改元素的可见性,仍然在文档流中,不会响应任何事件。visibility: hidden 样式的行为类似于 opacity: 0 和 pointer-events: none 的组合。
Chrome 浏览器 JavaScript 引擎执行代码主要分为了三个过程:
重点看下编译阶段,这个阶段的核心是执行上下文的创建。
执行上下文的创建离不开 JavaScript 的运行环境,JavaScript 运行环境包括全局环境、函数环境和 eval。
全局环境和函数环境的创建过程如下:
每进入一个不同的运行环境时,JavaScript 都会创建一个新的执行上下文,该过程包括:
什么是变量对象呢?每个执行上下文都会有一个关联的变量对象,该对象上会保存这个上下文中定义的所有变量和函数。
而在浏览器中,全局环境的变量对象是 window 对象,因此所有的全局变量和函数都是作为 window 对象的属性和方法创建的。相应的,在 Node 中全局环境的变量对象则是 global 对象。
在 JavaScript 中,函数是一等公民,我们编写的代码更多是通过函数为基本单元来进行组织的。JavaScript 运行环境中出现次数更多的自然是函数环境,每进入一个新的函数环境,都会按照上述三个步骤创建一个新的执行上下文。
在函数环境下创建 VO 的时候,会创建我们熟知的 arguments 对象,然后检查函数声明和变量声明。
上述变量声明和函数声明的处理过程,便是我们常说的变量提升和函数提升,其中函数声明提升会优先于变量声明提升。
因为变量提升容易带来变量在预期外被覆盖掉的问题,同时还可能导致本应该被销毁的变量没有被销毁等情况。因此 ES6 中引入了let 和 const 关键字,从而使 JavaScript 也拥有了块级作用域。
作用域分为静态作用域和动态作用域,在 JavaScript 中采用的是静态作用域(等同于词法作用域)。静态作用域的特点是,变量在编译阶段会生成一个确定的作用域。
作用域 = 词法环境 = 当前执行上下文
词法环境又分为两种:
词法环境由两个成员组成:
通过外部词法环境的引用,作用域可以层层拓展,建立起从里到外延伸的一条作用域链。当某个变量无法在自身词法环境记录中找到时,可以根据外部词法环境引用向外层进行寻找,直到最外层的词法环境中外部词法环境引用为null,这便是作用域链的变量查询。
有如下代码:
function foo() {
console.dir(bar);
var a = 1;
function bar() {
a = 2;
}
}
console.dir(foo);
foo();
打印结果如下:
可以看到:
JavaScript 会通过外部词法环境引用来创建变量对象的一个作用域链,从而保证对执行环境有权访问的变量和函数的有序访问。
编译阶段会进行变量对象(VO)的创建,该过程会进行函数声明和变量声明,这时候变量的值被初始化为 undefined。在代码进入执行阶段之后,JavaScript 会对变量进行赋值,此时变量对象会转为活动对象(Active Object,简称 AO),转换后的活动对象才可被访问,这就是 VO -> AO 的过程。
为了更好地理解这个过程,我们来看个例子,我们在foo函数中定义了变量b、函数c和函数表达式变量d:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
}
foo(1);
在执行foo(1)时,首先进入定义期,此时:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
进入执行期之后,会执行赋值语句进行赋值,此时变量b和d会被赋值为 2 和函数表达式:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 2,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
实际上在执行的时候,除了 VO 被激活,活动对象还会添加函数执行时传入的参数和arguments这个特殊对象,因此 AO 和 VO 的关系可以用以下关系来表达:
AO = VO + function parameters + arguments
虽然 JavaScript 代码的运行过程可以分为语法分析阶段、编译阶段和执行阶段,但由于在 JavaScript 引擎中是通过调用栈的方式来执行 JavaScript 代码的(下一讲会介绍),因此并不存在“整个 JavaScript 运行过程只会在某个阶段中”这一说法,比如上面例子中bar函数的编译阶段,其实是在foo函数的执行阶段中。
一般来说,当函数执行结束之后,执行期上下文将被销毁(作用域链和活动对象均被销毁)。但有时候我们想要保留其中一些变量对象,不想被销毁,此时就会使用到闭包。
在 JavaScript 中,this指向执行当前代码对象的所有者,可简单理解为this指向最后调用当前代码的那个对象。
根据 JavaScript 中函数的调用方式不同,this的指向分为以下情况。
function insertAfter(newEl, targetEl) {
const parentEl = targetEl.parentNode;
if (parentEl.lastChild === targetEl) {
parentEl.appendChild(newEl);
} else {
parentEl.insertBefore(newEl, targetEl.nextSibling);
}
}
今天在修复一个 bug 时,看到 bug 描述是从课程列表页进入课程详情页后,来回切换左侧 tab 菜单,然后点击课程详情页返回按钮,预期是返回课程列表页,实际上返回的是上一个 tab 菜单页。
于是查了下资料:HTML 中的 History length 属性用于返回当前浏览器窗口历史列表中 URL 的计数。该属性返回的最小值是 1,因为当前页面是在此刻加载的,而可以显示的最大计数为 50。做了下测试,从打开一个浏览器窗口起,每打开一个页面,length 值就会加一,首次打开浏览器时会有默认页面,此时 length 为 1。
回到上述提到的 bug,我看下了源代码,发现里面所有的 tab 菜单页都监听了顶部导航栏里的返回按钮点击事件,有的处理逻辑是根据 history.length 是否大于 1 判定是否要回退到课程列表页的。
<div id="test">
<span style="color:red">test1</span> test2
</div>
innerHTML 是 <span style="color:red">test1</span> test2
;outerHTML 是整段 HTML。
在前端工程化方兴未艾的今天,很多人会更多关注项目上线前的工程化问题,比如如何生成模版项目(脚手架)、代码测试、编译构建、流水线 CI 和 CD 等,很少会关注项目上线后的一些异常问题。对于前端异常,很多时候开发者始终处于一个被动感知的状态,当用户反馈到开发者这边时,往往线上已经出现大量 case 了。后端一般会有告警服务,前端在这方面的能力就比较缺失,所以如何用工程化的手段去解决异常监控就很有必要了。
按照 ECMA-262 里的规范来看,一共有如下类型的异常:
Error 是所有异常的基类,所有错误共享相同的属性:
当然,开发者也可以继承 Error 来自定义异常,一般而言一些库和框架会自定义异常。
前端监控异常的方法如下:
同步异常还比较好处理,因为会有完整的错误堆栈等信息,而异步错误就存在语焉不详的问题,因为异步错误缺失执行前的上下文环境,所以对于异步错误还需要借助前后埋点等方式作为辅助去定位错误源头。
function promiseAll (arr = []) {
return new Promise((resolve, reject) => {
let ans = []
let count = 0
arr.forEach((item, index) => {
Promise.resolve(item).then(res => {
ans[index] = res // index 块级作用域确保结果数组顺序和原数组一致,也可以在 for 循环里使用 let
count++ // 统计 resolve 个数
if (count === arr.length) {
resolve(ans)
}
}).catch(e => reject(e))
})
})
}
function task1 () {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
function task2 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 100)
})
}
let arr = [task1(), task2()]
promiseAll(arr).then(res => {
console.log('ans ===', res) // 输出 [1, 2]
})
页面布局分为流式布局、浮动布局和绝对定位布局三种,而 BFC 属于流式布局。
Formatting context(格式化上下文) 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。
具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。
得益于 MVVM 框架的流行,组件化方案成为了 webapp 开发中的标准,在业务开发中怎么区分基础组件和业务组件往往是需要一定技巧的。
我个人的思考总结起来有这么几个特点:低耦合、原子性、高复用。低耦合是指基础组件不应该和其它组件耦合在一起;原子性是指基础组件作为一个基本单元只需要关注前端 UI 层面的输入和输出,其本身是高度自治的;高复用是指基础组件可以和其它基础组件或者是业务组件组合成更加复杂的组件。
业务组件总结起来应该具有这么几个特点:高耦合、复杂性、低复用。高耦合是指我们在实现一个具体的业务需求时,往往需要组合基础组件成为一个新的组件;复杂性是指其除了需要关注前端 UI 层面的输入和输出,还需要关注和后端之间的交互;低复用是指业务组件一般是为了实现某个特定需求而开发出来的,当遇到不一样的需求时往往没办法实现复用。
实现方式有 3 种:
SSE 是 Server-Sent Events 的简称, 是一种服务器端到客户端(浏览器)的单项消息推送。对应的浏览器端实现 Event Source 接口被制定为HTML5 的一部分。不过现在IE不支持该技术。相比于 WebSocket,SSE 简单很多,服务器端和客户端工作量都要小很多、简单很多,同时实现的功能也要有局限。
如果 http 版本处于 2.0 以下,那么 http 是没办法做到服务器主动推送消息到客户端的。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。
也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。
和 WebSocket 相比,SSE 技术有如下特点:
关于 SSE 技术的具体细节,可以阅读阮一峰的博客Server-Sent Events 教程。
有一个未知的对象数组,数组的每一项存储的都是对象,现在需要对这个数组去重。这个和我们常见的对象数组不太一样,以往我们会根据对象数组里对象的某些属性去重,而这道题我们无法得知对象里的属性是什么,所以需要采取最直观的方式去重。
function dropDuplicate (arr = []) {
if (arr.length <= 1) return arr
const isSameObj = (a = {}, b = {}) => {
if (typeof a !== 'object' || typeof b !== 'object') return false
if (Object.keys(a).length !== Object.keys(b).length) return false
for (const [key, value] of Object.entries(a)) {
if (typeof value === 'object' || typeof b[key] === 'object') {
return isSameObj(value, b[key])
}
if (!b[key] || b[key] !== value) return false
}
return true
}
let dropIdx = []
for (var i = 0, len = arr.length; i < len - 1; i++) {
for (var j = i + 1; j < len; j++) {
if (isSameObj(arr[i], arr[j])) dropIdx.push(j)
}
}
dropIdx.forEach(index => arr.splice(index, 1, null))
return arr.filter(item => item !== null)
}
let objArr = [
{
name: 'chuck',
gender: 'male',
children: {
name: 'nick',
bro: []
}
},
{
name: 'chuck',
gender: 'male',
children: {
name: 'nick',
bro: []
}
},
{
name: 'henry',
gender: 'male'
},
{
name: 'mary',
gender: 'female'
},
{
name: 'mary',
gender: 'female'
}
]
console.log(dropDuplicate(objArr))
// 输出如下
// [
// {
// name: 'chuck',
// gender: 'male',
// children: { name: 'nick', bro: [] }
// },
// { name: 'henry', gender: 'male' },
// { name: 'mary', gender: 'female' }
// ]
我只想到了这种时间复杂度为 O(n * n) 的解法,如果看到的同学有 O(n) 的方案,欢迎补充。
提到代理我们会想到 nginx 和 Charles,nginx 可以直接将特定目录下的资源代理到指定的目录,其既可以是服务器上的资源,也可以是本地启动 docker 将文件系统挂起后的本地资源;Charles 更多时候是用来抓包各种请求,它也可以像 nginx 那样批量代理,比如针对某个域名路径后接通配符来达到批量代理的目标。
1、nginx
location /statics/ {
alias /project/statics/;
}
2、Charles
抓取到需要代理的请求后右击可以选择 map local,有时候如果想直接改线上代码,这种方式很合适。https://blog.csdn.net/Moonlight_16/article/details/118726585
// 请实现一个方法, 对传入的异步任务尝试n次
// 若异步任务小于等于n次执行的某次为fullfilled, 则返回这次成功的结果
// 若异步任务n次执行均为rejected, 则返回最后的一次错误
async function tryNTimes (asyncFn, n = 5) {
let err
let count = 1
while (n--) {
try {
console.log(`第${count++}次尝试`)
const ret = await asyncFn()
return Promise.resolve(ret)
} catch (e) {
err = e
continue
}
}
return Promise.reject(err)
}
function task () {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 生成一个[0, 10] 的整数
let random = Math.floor(Math.random() * 11)
console.log('生成的随机数:', random)
if (random < 2) {
console.log('任务成功')
resolve(random)
} else {
reject(new Error('任务失败'))
}
}, 1000)
})
}
tryNTimes(task)
运行结果如下:
function compose (middleware = []) {
return function (context, next) {
function dispatch (index) {
let fn = middleware[index]
if (index === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function () {
return dispatch(++index)
}))
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
有这样一段代码:
let t1 = setTimeout(() => {
console.log(1)
let t2 = setTimeout(() => {
console.log(2)
let t3 = setTimeout(() => {
console.log(3)
}, 3000)
}, 2000)
}, 1000)
这段代码将会在 1s、3s 和 6s 时分别打印 1、2、3。当定时器过多时,这种嵌套会导致代码臃肿。为了解决这个问题,思路就是如何实现定时器链式调用。
提起链式调用就会想到 ES6 中的 Promise.then,所以问题就是怎么把每个定时器转换为 Promise。默认情况下,每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用。
将一个定时器转换为 Promise 其实就是常见的 sleep 方法:
let sleep = function (time = 0) {
return new Promise(resolve => setTimeout(resolve, time))
}
接下来要做的事情就是自定义 .then 方法的返回值,为了实现定时器链式调用需要直接返回 sleep。
let t = sleep(1000).then(() => {
console.log(1)
return sleep(2000)
}).then(() => {
console.log(2)
return sleep(3000)
}).then(() => {
console.log(3)
})
Node 应用由模块组成,采用 CommonJS 模块规范。也就是说CommonJs是应用在node服务器端的,如果浏览器想使用CommonJs规范的话需要用 browserify 库来进行转化。
Node内部提供一个Module构建函数,所有模块都是Module的实例。
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
}
每个模块内部,都有一个module对象,代表当前模块。它有以下属性。
module.exports属性表示当前模块对外输出的接口,其它文件引入模块则使用 require,其有如下两个特性:
异步加载模块的方式。通过 define 传递一个函数来定义模块,引用模块则使用 require 函数,第一个参数是模块名,第二个参数则是回调函数,回调参数就是引入的模块名。
通过对 CommonJs、CMD、AMD 进一步处理,它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身。
export 导出,import 导入,可以实现按需加载、条件加载。与 commonjs 的区别如下:
和 img 元素表现类似,如果在 video 上添加背景色,会看到底部有多出来的空余部分。video 属于行内元素(demo),img 也可以看做是行内元素,所以出现留白问题是 line-height 和 vertical-align 造成的,解决办法如下:
我目前的开发方式是通过 docker 启动一个 container,然后在把本机的工作目录挂载到 container 里,这样的话很方便,具体可以阅读 https://docs.docker.com/get-started/06_bind_mounts/。在一次开发调试时,发现 Chrome 预览没有生效,经过查看资源请求信息,发现很多资源不是 from memory cache 就是 from disk cache,而这就导致更改一直未生效。我于是把 Chrome 控制面板中的 Disable cache 勾选了,刷新页面加载到了最新的本地资源。本着好奇心,我接下来花了点时间探索了下 Chrome 的缓存策略。
memory cache 是从浏览器的内存空间(RAM)中访问缓存信息,所以读写速度更快,但它的生命周期更短;而 disk cache 是从磁盘访问的,读写速度较慢,它属于持久缓存。发现一个有意思的地方,就是 css 资源是 from disk cache,而 js、图片、字体等资源则是 from memory cache。关于 Chrome 的缓存策略可以阅读 https://www.chromium.org/developers/design-documents/network-stack/disk-cache/。
浏览器开始渲染 标签或者解析完 标签的时刻就是页面白屏结束的时间点。也就是说从显示内容前的时间。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>白屏</title>
<script type="text/javascript">
// 不兼容performance.timing 的浏览器,如IE8
window.pageStartTime = Date.now();
</script>
<!-- 页面 CSS 资源 -->
<link rel="stylesheet" href="common.css">
<link rel="stylesheet" href="page.css">
<script type="text/javascript">
// 白屏时间�结束点
window.firstPaint = Date.now();
</script>
</head>
<body>
<!-- 页面内容 -->
</body>
</html>
白屏时间 = firstPaint - performance.timing.navigationStart;
白屏时间 = firstPaint - pageStartTime;
首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。
首屏时间计算方法有:
由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:
注意:首屏时间是包括了白屏时间的,因此可以使用 performance.timing.navigationStart 作为开始时间。
// 请实现一个 render 函数:该函数接受两个参数,一个是模版,一个是数据对象。
// 模版通过插值表达式引用数据对象上的数据,通过 render 函数能够把模版转换为真实的字符串。
let template = '大家好,我的名字叫做{{ name }},我是一名{{ job }}。'
let data = {
name: 'Henry',
gender: 'male',
job: 'software engineer'
}
function render (template = '', data) {
if (data === null) return
const MUSTACHE_REG = /\{\{.*?\}\}/ig
const KEY_REG = /(?<=\{\{(\s*?)).*?(?=(\s*?)\}\})/ig
return template.replace(MUSTACHE_REG, function (match = '') {
let key = match.match(KEY_REG)[0].trim()
return data[key]
})
}
console.log(render(template, data)) // 大家好,我的名字叫做Henry,我是一名software engineer。
function deepClone (origin = {}, target = {}, map = new WeakMap()) {
if (typeof origin !== 'object') return origin
if (origin === null) return null
if (origin instanceof RegExp) return new RegExp(origin)
if (origin instanceof Date) return new Date(origin)
if (origin instanceof Set) {
let _set = new Set()
origin.forEach(item => _set.add(item))
return _set
}
if (origin instanceof Map) {
let _map = new Map()
origin.forEach((val, key) => _map.set(key, val))
return _map
}
if (map.has(origin)) return map.get(origin)
map.set(origin, target)
for (const key in origin) {
if (origin.hasOwnProperty(key)) {
if (typeof origin[key] !== 'object') {
target[key] = origin[key]
} else {
target[key] = Array.isArray(origin[key]) ? [] : {}
target[key] = deepClone(origin[key], target[key], map)
}
}
}
return target
}
let set = new Set()
set.add(4).add(5).add(6)
let map = new Map()
map.set('name', 'chuck').set('gender', 'male')
let obj = {
a: 1,
b: {
c: '2',
d: [1, 2, 3]
},
reg: new RegExp('reg'),
date: new Date('2022/9/27'),
set,
map,
getName: () => {
return 'chuck'
},
undef: undefined,
empty: null,
symbol: Symbol('symbol')
}
let obj2 = {
a: 2,
next: obj
}
obj.next = obj2
let obj3 = Object.create(obj)
obj3.a = 1
console.log('deepClone', deepClone(obj))
function debounce(func, wait) {
let timer = null
let flag = true
return function () {
clearTimeout(timer)
if (flag) {
func.apply(this, arguments)
flag = false
}
timer = setTimeout(() => { flag = true }, wait)
}
}
在 vue 里使用:
methods: {
toPurchaseNow: debounce(async function () {
// do something
}, 1000)
}
Git 提交的工作流程是:工作区 ——> 暂存区 ——> 仓库区,如果要恢复已经提交的文件,那么就需要反过来执行这一个流程。
所以按照分类,提交的文件存在 3 种情况:
那么对应的就有 3 种解决方法,首先来看如何处理第一种情况。
1、对于本地分支
git branch -m oldName newName
2、对于远程分支
删除远程分支:
git push --delete origin oldName
再推送本地已更改分支到远程:
git push -u origin newName
当项目过大时,git clone 会出现超时失败,这时候我们可以只拉去最新的一次或者几次 commit:
git clone projectName --depth=1
如果需要获取历史提交代码,就需要重新拉取:
git pull --unshallow
monorepo 发布仓库,本地还处于 alpha 调试版本阶段,准备发版时发现已经落后于主分支了,rebase 的时候会出现很多冲突,但其实基本是版本冲突,可以使用 vscode 的 accept all xxx 来快速解决,具体可以看看 https://stackoverflow.com/questions/52288120/how-can-i-accept-all-current-changes-in-vscode-at-once。
客户端先检查本地是否有缓存以及缓存是否过期,如果过期,则像 CDN 边缘节点发起请求。CDN 边缘节点会检查用户请求数据的缓存是否过期,如果没有过期,直接响应用户的请求,完成一次 http 请求和响应。如果 CDN 缓存数据过期,那么还需要向源站发送回源请求(back to the source request),来拉取最新的数据。
CDN 的典型拓扑图如下:
CDN 边缘节点缓存策略因服务商不同而不同,但一般都会遵循 http 标准协议,通过 http 响应头中的 Cache-control: max-age 的字段来设置 CDN 边缘节点数据缓存时间。
当客户端向 CDN 节点请求数据时,CDN 节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN 节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN 服务商一般会提供基于文件后缀、目录多个维度来指定 CDN 缓存时间,为用户提供更精细化的缓存管理。
CDN 缓存时间会对“回源率”产生直接的影响。若 CDN 缓存时间较短,CDN 边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;若 CDN 缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。
CDN 边缘节点对开发者是透明的,相比于浏览器的强制刷新来使浏览器本地缓存失效,开发者可以通过 CDN 服务商提供的“刷新缓存”接口来达到清理 CDN 边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制 CDN 节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。
CDN 的分流不仅减少了用户的访问延时,也减少了源站的负载压力。但是缺点很明显,当源站数据更新时,如果 CDN 节点过多没有及时更新,那么用户访问到的还是过期的资源,涉及到 js 的脚本资源很可能会导致用户访问异常。
CDN 如果挂了,应用就会受到影响,所以可以通过启用本地资源的方式来作为兜底策略。代码实现如下:
<script src="http://cdn.static.runoob.com/libs/jquery/1.10.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/vendor/jquery-1.10.2.min.js"><\/script>')</script>
一般而言一个网站的资源存放于不同的域名,可以把本地资源路径替换为主域地址。可以阅读当CDN主域挂了该如何应对(前端网站容灾方案)。
域名发散作为网络性能优化手段之一,并不是说越多域名越好,因为 DNS 解析域名也会有开销。而且因为很多网站采用的是 https 协议,需要安装更多的证书,不利于快速部署。
// 写一个 Task 类
class Task {
constructor() {
// coding here
this.queue = []
this.flag = false
}
add(fn, context, ...args) {
// coding here
let task = {
fn,
context,
args: [...args]
}
this.queue.push(task)
return this
}
run() {
// coding here
let len = this.queue.length
const func = (index = 0) => {
if (this.flag) return Promise.reject(new Error('Follow-up tasks are stopped.'))
if (index === len) return Promise.resolve()
const task = this.queue[index]
return Promise.resolve(task.fn.apply(task.context, [() => func(++index), ...task.args]))
}
return func()
}
stop() {
// coding here
// this.queue = []
this.flag = true
}
}
// 满足
function task1(next) {
setTimeout(() => {
console.log(1)
next()
}, 1000)
}
function task2(next, a) {
// this.stop()
console.log(this.queue)
setTimeout(() => {
console.log(a)
next()
}, 1000)
}
function task3(next, b, c) {
setTimeout(() => {
console.log(b)
console.log(c)
next()
}, 1000)
}
const task = new Task()
task.add(task1).add(task2, task, 2).add(task3, task, 3, 4)
task.run()
// 备注:当任务函数执行 next 的时候,会跳转到下一个任务函数执行
class Queue {
constructor () {
this.tasks = []
}
task (time, fn) {
this.tasks.push({ fn, time })
return this
}
async start () {
for (const item of this.tasks) {
const { fn, time } = item
await this.timeout(time).then(fn)
}
}
timeout (time) {
return new Promise(resolve => setTimeout(resolve, time))
}
}
new Queue()
.task(1000, () => console.log(111))
.task(2000, () => console.log(333))
.task(1000, () => console.log(444))
.start()
lodash 中的这个方法是用于获取一个变量里的值的,因为很多时候通过点式访问很可能报错,所以希望对路径不存在的返回默认值。
function _get(obj, keys, defaultVal) {
let lists
if (Array.isArray(keys)) {
lists = keys
} else {
lists = keys.replace(/\[/g, '.').replace(/\]/g, '').split('.') // 字符串直接根据 . 分割为数组
}
let ret = lists.reduce((acc, curKey) => {
return (acc || {})[curKey] // 重点是利用好这个 acc 累积器,当属性不存在的时候返回的是 undefined
}, obj)
return ret || defaultVal
}
var obj = {
a: [
{
b: {
c: 3,
},
},
],
e: {
f: 1,
},
};
console.log(_get(obj, 'e.f')); // 1
console.log(_get(obj, ['e','f'])) // 1
console.log(_get(obj, 'a.x')); // undefined
console.log(_get(obj, 'a.x', '--')) // --
console.log(_get(obj, 'a[0].b.c')) // 3 => 'a.0.b.c' => ['a', '0', 'b', 'c']
console.log(_get(obj, ['a', 0, 'b', ,'c'])) // 3
如何理解浏览器的进程和线程呢?其实在浏览器中每打开一个 tab 页面,就开启了一个进程。由于在一个进程下,可以有多个线程,所以浏览器进程下又划分了 GUI 渲染线程和 JavaScript 引擎线程。由于 JavaScript 引擎线程在执行中是可能会更改 DOM 的,所以出于线程安全考虑,GUI 渲染线程和 JavaScript 引擎线程是互斥的,同一时间下只会有一个掌握有控制权(执行权)。
XSS 是常见的 Web 攻击技术之一.所谓的跨站脚本攻击指得是:恶意攻击者往 Web 页面里注入恶意 Script 代码,用户浏览这些网页时,就会执行其中的恶意代码,可对用户进行盗取 cookie 信息、会话劫持等各种攻击。
1.盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2.控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
3. 盗窃企业重要的具有商业价值的资料
4.非法转账
5.强制发送电子邮件
6.网站挂马
7.控制受害者机器向其它网站发起攻击
Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">
CSRF(Cross-site request forgery)跨站请求伪造,也被称为 “One Click Attack” 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与 XSS 非常不同,XSS 利用站点内的信任用户,而 CSRF 则通过伪装来自受信任用户的请求来利用受信任的网站。与 XSS 攻击相比,CSRF 攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比 XSS 更具危险性。
点击劫持是指在一个Web页面中隐藏了一个透明的iframe,用外层假页面诱导用户点击,实际上是在隐藏的frame上触发了点击事件进行一些用户不知情的操作。
if ( top.location != window.location ){
top.location = window.location
}
X-FRAME-OPTIONS是微软提出的一个http头,专门用来防御利用iframe嵌套的点击劫持攻击。并且在IE8、Firefox3.6、Chrome4以上的版本均能很好的支持。
DDOS 攻击,它在短时间内发起大量请求,耗尽服务器的资源,无法响应正常的访问,造成网站实质下线。
PC总端口数为65536,那么一个TCP(http也是tcp)链接就占用一个端口。操作系统通常会对总端口一半开放对外请求,以防端口数量不被迅速消耗殆尽。
一个线程对应处理一个http请求,那么如果并发数量巨大的话会导致线程频繁切换。而线程的上下文切换有时候并不是轻量级的资源。这导致得不偿失,所以请求控制器里面会产生一个链接池,以复用之前的链接。所以我们可以看作同域名下链接池最大为4~8个,如果链接池全部被使用会阻塞后面请求任务,等待有空闲链接时执行后续任务。
在服务端通常都对同一个客户端来源设置并发阀值避免恶意攻击,如果浏览器不对同一域名做并发限制可能会导致超过服务端的并发阀值被 BAN 掉。
为了防止两个应用抢占资源时候导致强势一方无限制的获取资源导致弱势一方永远阻塞状态。
样式隔离主要有以下几种方案:
js 隔离方案:
面试中面试官让手写一个缓存函数,然后自己其实没有怎么看过这方面知识,按照自己的认知利用闭包和 map 写了出来。其实我之前在看 vue 源码的时候有注意到计算属性是基于缓存函数实现的,但时间有点久了,后面搜了下有比较简洁的写法:
function memorize (func, context) {
let cache = Object.create(null)
context = context || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(context, key)
}
return cache[key]
}
}
class EventEmitter {
constructor () {
if (!EventEmitter.instance) {
this.task = {}
EventEmitter.instance = this
}
return EventEmitter.instance
}
on (type, handler) {
if (!this.task[type]) this.task[type] = []
this.task[type].push(handler)
}
emit (type) {
if (!this.task[type]) return
const args = Array.from(arguments).slice(1)
const handlers = this.task[type]
for (var i = 0, len = handlers.length; i < len; i++) {
const handler = handlers[i]
handler(...args)
}
}
}
// var EventEmitter = (function () {
// var instance
// return function () {
// if (!instance) {
// instance = new BaseEventEmitter()
// return instance
// }
// return instance
// }
// })()
const eventBus = new EventEmitter()
const eventBus1 = new EventEmitter()
console.log(eventBus === eventBus1) // 打印输出: true
function handleClick(param1, param2) {
console.log(param1, param2)
}
eventBus.on('click', handleClick)
eventBus.emit('click', 'foo', 'bar') // 打印输出: foo bar
我们经常会看到同步任务和异步任务的文章,而且对这些概念的认知都根深蒂固了,但什么是任务呢?下面是一段对任务的描述:
A task is any JavaScript code which is scheduled to be run by the standard mechanisms such as initially starting to run a program, an event callback being run, or an interval or timeout being fired. ... An event fires, adding the event's callback function to the task queue.
任务是由标准机制安排运行的任何 JavaScript 代码,如最初开始运行程序,事件回调被运行,或间隔或超时被触发。一个事件被触发,将事件的回调函数添加到任务队列中。
有这样两个 number 类型的数组,需要求两个数组之间的交集。如果不借助 API,最先想到的是通过 for 循环来解决。
function findCommon (arr1, arr2) {
var arr = []
for (var i = 0; i < arr1.length; i++) {
for (var j = 0; j < arr2.length; j++) {
if (arr1[i] === arr2[j]) arr.push(arr1[i])
}
}
return arr
}
这样虽然能满足要求,但是太原始了,时间复杂度始终是 O(m*n)。所以优化的点就是把时间复杂度 O(m*n) 给 降低到 O(m+n)。
因此想到了滑动比较,滑动比较需要对数组排序,这样才好利用排序的特征。滑动比较的具体操作是,采用双下标或者双指针,从头开始比较。如果一个值比另一个小,那么只需要移动小的那个的下标或者指针;如果两个值相等,那么这个值在交集里,保存该值,然后下表或者指针同时往前移动;重复前两步操作,直到下标或者指针超过数组长度为止。
有了这样的思路,接下来就是具体的编码实现了。第一步需要对数组进行排序,这里对两个数组进行升序排序。
function findCommon (arr1 = [], arr2 = []) {
arr1 = arr1.sort((a, b) => a - b)
arr2 = arr2.sort((a, b) => a - b)
var i = 0,
j = 0,
arr = []
while (i < arr1.length && j < arr2.length) {
if (arr1[i] < arr2[j]) {
i++
} else if (arr1[i] === arr2[j]) {
arr.push(arr1[i])
i++
j++
} else {
j++
}
}
return arr
}
由于多了一步排序操作,所以整体的复杂度,还需要加上 sort 方法的时间复杂度。对于 sort 这个 API,不同浏览器厂商的实现方式不一样,具体可以查看深入浅出 JavaScript 的 Array.prototype.sort 排序算法。
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.