关于this指向问题
- 默认是全局对象, 严格模式下为 undefined
- 普通函数的 this 在调用时确定
- 谁调用, 谁就是this.
- 作为对象方法, 原型链方法, 对象的 getter/setter 方法时也是如此.
- 使用 bind call apply 可绑定 this. 替代调用时确定的规则.
- 箭头函数的this继承自作用域链上层
- new 构造函数, this为正在构造的对象
- dom 事件处理函数, this为触发事件的元素.
- on-event 内联函数, this 为绑定的元素.
具体场景
1 | const log = () => console.log.apply(console, arguments) |
关于call, bind, apply的实现
1 | const log = console.log.bind(console) |
关于屏幕的适配
媒体查询
1 | @meida screen and (max-wdith: 360px) { |
动态修改(rem)
在国内一般都是用的rem规范, 默认是1rem指根元素fontSize的值. 国外用的一般em, em是指父元素的fontSize.
当然也有用vm, vh的。
1 | function setBaseFont() { |
然后有两中办法使用,一种是用css把所有的px变成rem。 另一种是插件在vscode中下载px2rem插件自动转成rem
当然如果是一个模块化工程中,可以通过设置webpack引用的postcss-plugin-px2rem
插件来解决
- 通过使用css
1
2
3
4@function px2rem($px) {
$rem: 37.5;
@return ($px / $rem) + rem;
} - 通过使用webpack
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
loader: require.resolve("postcss-loader"),
options: {
postcssOptions: {
plugins: [
require("postcss-preset-env")({
autoprefixer: {
overrideBrowderslist: "andoroid >= 4.3",
}, // 添加webkit, mozilla前缀
stage: 3,
}),
require("postcss-plugin-px2rem")({
rootValue: 75, // 根据index.html的doucment fontsize设置
minPixelValue: 2, // 设置要替换的最小像素值
}),
],
},
},
},
自定义事件
- 原生javascript自带的Event类或者CustomEvent实现
1 | let eve = new Event('custom') |
- 自己编写EventEmitter类实现
1 | class EventEmitter() { |
各种http请求方式
fetch的方式请求数据,
- fetch 不支持同步请求
- fetch 不支持取消一个请求
- fetch 无法查看请求的进度
- fetch 只有遇到网络错误的时候才会reject这个promise请求
对于options、跨域等错误响应,Promise也是resolved状态,需要用response.ok来进行判断
用法如下:
1
2
3
4
5
6
7
8
9
10
11
12fetch('/api/user.json?id=2', {
credentials: 'include' // 携带cookie信息
})
.then((response) => {
// 返回的response是一个ReadableStream对象,需要调用对象的json方法进行格式化
if(!response.ok) Promise.reject('cannot get data')
return response.json();
})
.then(data => {
console.log(data);
})
.catch(err => console.log(err))ajax
ajax的readState有五种状态:- 0(未初始化)还没有调用open()方法
- 1(载入)。已经调用 open()方法,但尚未调用 send()方法
- 2(载入完成)send()方法执行完成,
- 3(交互)正在解析响应内容
- 4(完成)响应内容解析完成,可以在客户端调用了
1
2
3
4
5
6
7
8
9
10
11
12
13
14const ajax = function (method, path, headers, data, runCallBack) {
var xhr = new XMLHttpRequest()
xhr.open(method, path, true)
Object.entries(headers).forEach(([header, value]) => {
xhr.setRequestHeader(header, value)
})
xhr.onprogress = (event) => {}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
runCallBack(xhr.response)
}
}
xhr.send(data)
}
关于axios用法
axios 是一个流行的http请求库- 默认axios会把要发送的JavaScript对象数据序列化为JSON
- 支持请求拦截和响应拦截
- axios.all 方法支持并发
- axios.defaults 可以全局配置默认值
- axios.CancelToken实例支持取消请求
- 可以查看请求进度
用法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74import axios from 'axios'
import qs from 'qs'
const source = axios.CancelToken.source()
const baseConfig = {
timeout: '2000',
// baseURL将自动加在url前面, 除非url是一个绝对URL。
baseURL: 'https://some-domain.com/api/',
// 上传处理进度事件
onUploadProgress: function (progressEvent) {
console.log(`上传进度${progressEvent.loaded / progressEvent.total * 100}%`)
},
// 下载处理进度事件
onDownloadProgress: function (progressEvent) {
console.log(`下载进度${progressEvent.loaded / progressEvent.total * 100}%`)
},
cancelToken: source.token
}
const options = {
headers: {
'Content-Type': 'application/x-www-form-urlencode'
},
url: '/user/all',
method: 'post',
withCredentials: false, // 跨域请求时是否需要使用凭证(Cookie)
data: {
username: 'woyao'
},
}
const createRequest = (baseConfig, options) => {
// 使用create创建一个axios实例
const instance = axios.create(baseConfig)
instance.interceptors.request.use(config => {
// 在发送请求之前做些什么, 比如设置一个全局的loading组件显示
switch (config.headers && config.headers['Content-Type']) {
case 'application/x-www-form-urlencoded':
// 这里也可以自己实现一个serialize函数
config.data = qs.stringify(config.data)
break
case 'multipart/form-data':
if (data instanceof HTMLFormElement) {
data = new FormData(data)
} else {
Promise.reject('data type is not valid of your ContentType defined')
}
break
case 'application/json':
break
default:
break
}
return config
}, error => {
// 对请求错误做些什么
return Promise.reject(error)
})
instance.interceptors.response.use((response) => {
// 对返回的数据进行拦截处理
// 比如关闭一个全局的loading组件
return response
}, (error) => {
// Do something with response error
return Promise.reject(error)
})
return instance(options)
}
createRequest(options).then().catch()
// 手动取消请求
// source.cancel('Operation canceled by the user.')
EventLoop事件机制
主线程(javaScript引擎线程)从”任务队列”中读取事件,这个过程是循环不断的, 所以整个的这种运行机制又称为Event Loop(事件循环)。
讲白了这个机制是因为js单线程的原因。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
当事件触发线程执行的时候,此时cpu空闲,如果非要等事件触发线程从阻塞状态变成就绪状态肯定是不行的。我们应该要用并发机制。在一个时间片段中多个线程可以交替执行。尽可能的是cpu跑起来。EventLoop就是一种并发机制策略。
在任务队列中:
任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
EventLoop:
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件
- 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件发生,找出事件对应的异步任务放入执行栈,主线程结束等待状态,开始执行。
- 主线程不断重复上面的第三步
注意: IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程在读取”任务队列”就是读事件,从而知道哪些异步任务可以放入执行栈。另外如果一个事件被添加了定时器,只有到了规定的时间才能返回主线程
另外既然扯到了EventLoop就不得说说
Process.nextTick
、setImmediate
、setTimeout
、requestAnimationFrame
- Process.nextTick(() => {})
在当前”执行栈”的尾部到下一次主线程读取任务队列之前触发回调函数
也就是说当前执行栈的最后面执行,下一次执行栈之前执行
另外不要写下面的死递归代码
1
2
3
4 // 当前的执行栈会永远执行不完,而任务队列永远读不了
process.nextTick(function foo() {
process.nextTick(foo);
});
setImmediate
当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行
和setTimeout(() => {}, 0)很相似setTimeout(() => {}, time)
过了time时间点以后, 下一次主线程读取任务队列时触发回调函数
- requestAnimationFrame
同setTimeout, 但是更流畅,一般用这个api实现函数节流
canvas的用法
canvas怎么用具体还是看canvas的mdn文档. api比较多,功能很强大。比较流行写h5游戏,
处理视频, 图表生成。
- 画布生成图像
1 | const canvas = document.getQuerySelector('canvas') |
- 图像变成灰度图
1 | const filterColor = function(context){ |
继承方式与原型链
首先讲一下javascript中原型链的一些定义:
- 每个对象都有
__proto__
属性,用于指向创建它的构造函数的原型对象 - 每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象
在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数) 属性 - 实例由构造函数初始化,可以继承创建它的构造函数的原型对象的方法
1 | const log = console.log.bind(console) |
了解了上面的定义之后讲讲在new一个构造函数生成一个实例的时候做了什么操作:
- 创建一个对象,继承自一个原型对象,就是指定实例对象的
__proto__
为构造函数的原型对象 - 构造函数执行,this指定为这个实例对象
- 如果构造函数返回了一个对象,则该对象取代步骤一: 创建的对象
- 如果没有,就不替换
1 | // func是构造函数 |
继承方式
- 构造函数继承
缺点: 父类的原型对象的方法, 子类拿不到
1 | function Parent1() { |
- 原型链对象继承
缺点: 实例对象因为都是继承了父类的实例,通过对父类实例的修改
会对各个实例之间都产生影响
1 | const log = console.log.bind(console) |
- 组合继承
1 | // 第一版: |
DOM常见操作
节点查找
1 | const e = (element, attribute) => element.querySelector(attribute) |
节点创建
1 | // 创建文本节点 |
节点修改(添加,删除,替换)
1 | parent.appendChild(child) |
节点属性
1 | element.setAttribute(name, value) |
节点关系
1 | parentNode :每个节点都有一个parentNode属性,它表示元素的父节点。Element的父节点可能是Element,Document或DocumentFragment; |
样式操作
1 | elem.style.color = 'red'; |
jquery常用api
选择器
普通选择器
find
siblings
closest, parent
dom 操作
append
remove
empty
show, hide, toggle
class 操作
addClass removeClass
toggleClass
属性、特性操作
attr, prop, data , prop 用于 true false 这样的布尔值属性
removeAttr
取值
val
text
html
reflect, proxy, Symbol的用法
==Proxy== 为其他对象提供一种代理以控制对这个对象的询问
大白话就是你要做的事情我给你来做,如果事情我能做的话, 这样事情就可以按照我想要的意愿发生1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 其实也就是个代理模式罢了, es6创建了一个Proxy类
// 对代理模式不懂可以看看我的设计模式文章(https://juejin.cn/post/6960665002449731598)
// 给target对象提供一个代理,通过handler对target做操作
// 大白话就是你要做的事情我给你来做,如果事情我能做的话, 这样事情就可以按照我想要的意愿发生
// Proxy(target, handler)
/*
es6 实现的Proxy代理类中定义了下面常用的劫持方法:
1. apply(target, thisArgument, ...args): 在代理对象调用的时候劫持
2. construct(target, ...args): new 操作的时候进行劫持
3. defineProperty(target, prop, attributes): Object.defineProperty的时候劫持
4. get(target, prop, receiver): 获取属性值的时候劫持
5. has(target, prop): in 操作的时候劫持
6. set(target, prop, value, receiver): 设置属性的时候劫持
7. ownKeys(target): Object.getOwnPropertyNames和Object.geetOwnPropertySymbols调用劫持
*/
const target = {
type: "human",
}
const handler = {
// 我想要获取属性的时候结果是如下的
get: function(target, prop, receiver) {
if (prop === "type") {
return "pig";
}
// return Reflect.get(target, prop, receiver)
return Reflect.get(...arguments);
},
// 你说他的名字是xxx, 我说他是个xxx
set: function(target, prop, value) {
if (prop === "name") {
target.name = "万恶的资产阶级";
}
}
}
const proxy = new Proxy(target, handler)
// 事情按照我想要的情况发生了
console.log(proxy.type) // pig
proxy.name = '某公司'
log(proxy.name)==reflect== 是一个内置的对象,它提供拦截 JavaScript 操作的方法
虽然和Proxy一样都是代理拦截。但是reflect不改变事情发生的结果
大白话就是你要做的事情我给你来做,如果事情我能做的话,我就帮你做了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 其实reflect这个对象主要是用来配合Proxy这个构造对象来用的
// 在Proxy的handler中所有的劫持方法, reflect都有
// 一件事情本来是什么样的我可以通过reflect调用来知道
// 大白话就是你要做的事情我给你来做,如果事情我能做的话,我就帮你做了
const handler = {
// 我想要获取属性的时候结果是如下的
get: function(target, prop, receiver) {
if (prop === "type") {
return "pig";
}
// 如果prop不是type的话,属性值原本是啥样就是啥样
// return Reflect.get(target, prop, receiver)
return Reflect.get(...arguments);
},
}
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
// 'color' in duck
Reflect.has(duck, 'color')
// Object.getOwnPropertyNames(duck)
Reflect.ownKeys(duck)==Symbol== 生成一个全局唯一的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39var sym1 = Symbol('foo');
var sym2 = Symbol('foo');
console.log(sym1 === sym2) // false
const obj = {
[Symbol('a')]:'Hello',
[Symbol('b')]:'world'
}
// Object.getOwnPropertyNames 不能获取key为Symbol类型的属性
// [Symbol(a), Symbol(b)]
const a = Object.getOwnPropertySymbols(obj)
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
}
// for...of, for...in 中也不能获取到Symbol属性
// ["enum", "nonEnum", Symbol(my_key)]
Reflect.ownKeys(obj)
// Symbol.for
// 使用给定的key搜索现有的symbol,
// 如果找到则返回该symbol。否则将使用给定的key在全局symbol注册表中创建一个新的symbol
// Symbol.for()会被登记在全局环境中
let s1 = Symbol.for('foo')
let s2 = Symbol.for('foo')
console.log(s1 === s2) // true
// Symbol.keyFOr
// 从全局symbol注册表中,为给定的symbol检索一个共享的symbol key
// Symbol.for 是的s1登记在全局注册表中
let s1 = Symbol.for("foo")
console.log(Symbol.keyFor(s1)) // "foo"
let s2 = Symbol("foo")
Symbol.keyFor(s2) // undefined
函数防抖与节流
防抖通常被叫做debounce, 节流叫做throttle. 它们的作用是用来控制事件对应的异步任务的执行频率。
简单来讲就是不能让一些异步任务总是在每一轮EventLoop中的主线程中老是执行。因为这样就抢占了
其他任务被执行的快慢。而本身自己也不需要执行那么多次。
debounce
这个函数的大白话就是:多次执行只有一次有效,讲白了就是希望当前事件连续发生时对应的所有执行任务的最后一个任务生效,另外有些情况也会希望事件第一次发生的时候也运行一次
- 常被用到的场景
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
1 | const log = console.log.bind(console) |
1 | // 上面的debounce的写法不太好 |
1 | // 上面写的debounce都是当前事件连续发生时所对应的执行任务的最后一个任务生效 |
throttle
这个函数的大白话就是:技能有cd,只有冷却时间到了以后才能发出技能.讲白了就是希望当前事件连续发生时对应的所有执行任务中满足到了冷却时间点的任务被执行
- 常被用到的场景
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
1 | // 好吧,场景还是鼠标在页面上连续移动的时候,弹出恶意广告。 |
js内存管理
内存生命周期
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还
内存分配
这里我是抄的这个人的关于垃圾回收机制的文章和mdn的js内存管理 写的都挺好的。
- JavaScript 在定义变量时就完成了内存分配
1 | // 在js中一切都是对象,对象声明赋值(定义)以后就会自动分配内存 |
使用值的过程实际上是对分配内存进行读取与写入的操作
内存释放-垃圾回收机制(跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它
大白话就是:监视所有对象,并删除那些不可访问的对象
js的垃圾回收机制算法如下:引用计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 引用计数垃圾收集
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o = {
a: {
b:2
}
};
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
// 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
var oa = o2.a;
// 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
o2 = "yo"
// a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
oa = null循环引用
该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收(通过标记清除算法解决)。1
2
3
4
5
6
7
8
9
10
11/* 注意在js的root中找不到o,o2的定义,会被标记清除 */
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();标记清除
设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。垃圾回收器获取根并“标记”(记住)它们。
然后它访问并“标记”所有来自它们的引用。
然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
以此类推,直到有未访问的引用(可以从根访问)为止。
除标记的对象外,所有对象都被删除。
解决了上述的循环引用因为引用计数不会被回收的问题.
具体实例:
- 编写一段代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function marry (man, woman) {
woman.husban = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
})- 在引用标记内存结构如下
- 删除以下引用
1
2delete family.father;
delete family.mother.husband;- 删除后的内存结构如下


5. 然后删除famlily的引用,根据标记清除的原则后内存结构如下
1
family = null;
