JS解析

数据类型

基本数据类型

string 、number、 boolean、null、undefine、symbol、bigInt

引用数据类型

function、array、RegExp、Date、Math

this指向

(一)普通函数的调用,this指向的是Window

1
2
3
4
5
6
7
var name = '卡卡';
function cat(){
var name = '有鱼';
console.log(this.name);
}
cat(); //卡卡

(二)对象的方法,this指的是该对象

  1. 一层作用域时,this指的是该对象
1
2
3
4
5
6
7
8
9
var name = '卡卡';
var cat = {
name:'有鱼',
eat:function(){
console.log(this.name);
}
}
cat.eat();//有鱼

  1. 多层作用域时,this指的是距离方法最近的一层对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var name = '卡卡';
var cat = {
name:'有鱼',
eat1:{
name:'年年',
eat2:function(){
console.log(this.name);
}
}
}
cat.eat1.eat2();//年年

var eat3 = cat.eat1.eat2;
eat3(); // 卡卡

(三)构造函数的调用,this指的是实例化的新对象

1
2
3
4
5
6
7
8
9
var name = '卡卡';
function Cat(){
this.name = '有鱼';
this.type = '英短蓝猫';
}
var cat1 = new Cat();
console.log(cat1);// 实例化新对象 Cat {name: "有鱼", type: "英短蓝猫"}
console.log(cat1.name);// 有鱼

(四)apply和call调用时,this指向参数中的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = '有鱼';
function eat(){
console.log(this.name);
}
var cat = {
name:'年年',
}
var dog = {
name:'高飞',
}

eat.call(cat);// 年年
eat.call(dog);// 高飞

(五)匿名函数调用,指向的是全局对象

1
2
3
4
5
6
7
8
var name = '卡卡';
var cat = {
name:'有鱼',
eat:(function(){
console.log(this.name);
})()
}
cat.eat; //卡卡

(六)定时器中调用,指向的是全局变量

1
2
3
4
5
6
7
var name = '卡卡';
var cat = setInterval(function(){
var name = '有鱼';
console.log(this.name);// 卡卡
clearInterval(cat);
},500);

(七)箭头函数调用

  如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'window';
var student = {
name: '若川',
doSth: function(){
// var self = this;
var arrowDoSth = () => {
// console.log(self.name);
console.log(this.name);
}
arrowDoSth();
},
arrowDoSth2: () => {
console.log(this.name);
}
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'

注意

①普通函数的调用,this指向的是window

②对象方法的调用,this指的是该对象,且是最近的对象

③构造函数的调用,this指的是实例化的新对象

④apply和call调用,this指向参数中的对象

⑤匿名函数的调用,this指向的是全局对象window

⑥定时器中的调用,this指向的是全局变量window

⑦.箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象

⑧.onclick和addEventerListener是指向绑定事件的元素(ev.currentTarget)

call、apply、bind的原理及实现

call

  改变当前函数this指向,并立即执行

1
2
3
4
5
6
7
Function.prototype.mycall = function(context,...args){
context = (context == null || context == undefine)?window:new Object(context)
context.fn = this
context.fn = (...args)
delete context.fn

}

apply

  将函数中的this指向传入的第一个参数,第二个为数组,和call方法作用相同,参数不同,立即执行

1
2
3
4
5
6
7
8
Function.prototype.myapply = function(context,args){
context = (context == null|| context == undefine)?window:new Object(context)
context.fn = this
if(!args) return context.fn
let r = eval('context.fn('+args+')')
delete context.fn
return r
}

bind

  bind() 方法创建一个新的函数,不自动执行,需要手动调用bind() 。这个新函数的 this 被指定为 bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.mybind = function(context,...args1){
let that = this
function fbind(...args2){
return that.call(this.instanceof fbind? this :context,...args1,...args2)
}
function Fn(){
Fn.prototype = this.prototype
fbind.prototype = new Fn()
return fbind
}
}

变量提升

  变量提升是指在js代码执行过程中,js引擎把变量的声明部分和函数声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值undefined

  js在编译阶段的时候,会搜集所有的变量声明并且提前声明变量,而其他的语句都不会改变他们的顺序,因此,在编译阶段的时候,第一步就已经执行了,而第二步则是在执行阶段执行到该语句的时候才执行。

执行上下文

  执行上下文是js代码执行的一个环境,存放了代码执行所需的变量,变量查找的作用域链规则以及this指向等。在编译阶段创建

  1. 全局执行上下文:当进入全局代码时会进行编译,在编译中创建全局执行上下文,并生成可执行代码
  2. 函数执行上下文:执行代码的过程中,如果遇到函数调用,会编译函数内的代码和创建函数执行上下文,并创建可执行代码
  3. eval执行上下文::当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文

作用域

  1. 全局作用域:不在函数内部作用域和块级内部作用域外的作用域
  2. 函数作用域:在函数内部的作用域
  3. 块级作用域:在花括号{}内部的作用域

注意:

  ①对象的{}不属于块级作用域,像for(){},if(){},else{},try{},cath(){}等等的花括号才是块级作用域

  ②对象的{}的作用域是什么作用域取决于对象所处的作用域,比如对象在全局作域 下定义的,那么对象的{}的作用域就是全局作用域

作用域链

  内部作用域访问外部作用域的变量,采取的是链式查找的方式来决定取哪个值,这种结构我们称为作用域链,采取就近原则的方式向上一级一级的作用域来查找变量值,最顶级是全局作用域,如果到全局作用域也没找值,那么就会报错。

闭包

概念

  如果一个函数访问了此函数的父级及父级以上的作用域变量,那么这个函数就是一个闭包。即函数不在当前语法作用域中执行,而是在当前语法作用域外执行,但仍然保持对当前语法作用域的引用,这种引用叫闭包

特点

  1. 被闭包函数访问的父级及以上的函数的局部变量(如范例中的局部变量 i )会一直存在于内存中,不会被JS的垃圾回收机制回收。
  2. 闭包函数实现了对其他函数内部变量的访问。(函数内部的变量对外是无法访问的,闭包通过这种变通的方法,实现了访问。)

用途

  1. 访问函数内部的变量
  2. 让变量始终保持在内存中
  3. 使用不当会导致内存泄漏

new 的原理

  1. 创建一个空对象作为返回实例
  2. 将这个空对象的原型,指向构造函数的prototype属性
  3. 将这个空对象赋值给构造函数内部的this关键字
  4. 开始执行构造函数内部的代码
  5. 返回这个对象
    1
    2
    3
    4
    5
    function myNew(fn,...arg){
    let obj = Object.create(fn.prototype)
    let result = fn.apply(obj,arg)
    return typeof result === 'object' ? result : obj;
    }

原型与原型链

  在默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性(是一个指针)指向 prototype 属性所在的函数

1
2
3
person1.constructor == Person
Person.prototype.constructor = Person
person1.__proto__ == Person.prototype

注意: 原型对象(Person.prototype)是 构造函数(Person)的一个实例。

原型与原型链参考文章

事件处理机制

  1. 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
  2. 目标阶段:真正的目标节点正在处理事件的阶段;
  3. 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

模块化

  模块化开发是一种管理方式,一种生产方式,一种解决问题的方案。一个模块就是实现某个特定功能的文件,我们可以很方便的使用别人的代码,想要什么模块,就引入那个模块。

  防止出现命名冲突,污染作用域等一系列问题

Iterator迭代器

  迭代器对象本质上,就是一个指针对象。通过指针对象的next(), 用来移动指针。

1
2
3
4
5
6
7
8
9
10
11
<!--JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。-->
var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
console.log(a); // 0 1 2 3
}

for (let a of arr) {
console.log(a); // a b c d
}

Promise

promise共有三种状态,pending,fulfilled,rejected

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// 自定义实现promise
class MyPromise {
constructor(executor) {
let _this = this
this.state = 'pending' // 当前状态
this.value = undefined // 存储成功的值
this.reason = undefined // 存储失败的值
// 利用发布订阅模式,让Promise支持异步
this.onFulfilledFunc = [] // 存储成功的回调
this.onRejectedFunc = [] // 存储失败的回调

function resolve (value) {
// Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态了。因此我们在更新状态时要判断,如果当前状态是pending(等待态)才可更新
if (_this.state === 'pending') {
_this.value = value
//依次执行成功回调
_this.onFulfilledFunc.forEach(fn => fn(value))
_this.state = 'resolved'
}
}

function reject (reason) {
// Promise对象已经由pending状态改变为了成功态(resolved)或是失败态(rejected)就不能再次更改状态了。因此我们在更新状态时要判断,如果当前状态是pending(等待态)才可更新
if (_this.state === 'pending') {
_this.reason = reason
//依次执行失败回调
_this.onRejectedFunc.forEach(fn => fn(reason))
_this.state = 'rejected'
}
}

try {
// 当实例化Promise时,构造函数中就要马上调用传入的executor函数执行
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
_resolvePromise (promise2, x, resolve, reject) {
// 如果返回了自己的Promise对象,状态永远为等待态(pending),再也无法成为resolved或是rejected,程序会死掉,因此首先要处理它
if (promise2 === x) {
reject(new TypeError('Promise存在循环引用'))
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// x可能是一个promise
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, (y) => {
_resolvePromise(promise2, y, resolve, reject)
})
} else {
resolve(x)
}
} catch (err) {
reject(err)
}
} else {
//否则是个普通值
resolve(x)
}
}
then (onFulfilled, onRejected) {
let promise2
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (val) { return val }
onRejected = typeof onRejected === 'function' ? onRejected : function (reason) { throw reason }

if (this.state === 'resolved') {
promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
this._resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})
}

if (this.state === 'rejected') {
promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
this._resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})
}

if (this.state === 'pending') {
promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledFunc.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
this._resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})

this.onRejectedFunc.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
this._resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
}, 0);
})
})
}

return promise2
}
}


Generator

什么是Generator

&emsp;&emsp;是ES6提供的一种异步编程解决方案,语法不同于普通函数;简单的把Generator 理解为一个状态机,封装了多个内部状态。执行Generator 函数会返回一个迭代器对象,可以通过调用迭代器next依次遍历Generator函数内部的每一个状态。

Generator的特征

  1. function关键字与函数名之间有个星号;
  2. 函数内部使用yield表达式

Generator的使用

1
2
3
4
5
6
7
8
9
10
11
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

async/await

&emsp;&emsp;声明异步函数,遇到await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,继续执行await后面的代码。

优缺点

  1. 相对于promise,async/await处理 then 的调用链,代码要清晰很多,几乎和同步代码一样
  2. 滥用 await 可能会导致性能问题,因为 await 会阻塞代码

事件循环

定义

&emsp;&emsp;事件循环,把异步任务的回调部分交给事件循环,等时机合适交还给 JS 线程执行。事件循环并不是 JavaScript 首创的,它是计算机的一种运行机制。

&emsp;&emsp;所有任务都可以分成同步任务和异步任务,当任务进入执行栈,会判断任务是同步还是异步任务,如果是同步任务进行主线程,异步任务进入异步队列(event Queue),等待主线程中的任务执行完毕后,会在异步队列中读取相应的任务(先进先出),推入主线程执行,这个上述过程不断重复就是事件循环(Event Loop)

&emsp;&emsp;事件循环是由一个队列组成的,异步任务的回调遵循先进先出,在 JS 引擎空闲时会一轮一轮地被取出,所以被叫做循环。

垃圾回收(GC)

&emsp;&emsp;是清理程序不用的内存或者是之前用过了,以后不会再用的内存空间。

回收策略

&emsp;&emsp;垃圾回收主要也是针对堆内存中的对象,对于栈中的非闭包变量,会被栈执行上下文切换时直接出栈,释放了内存空间

内存泄漏

  1. 内存泄漏:当不再用到的对象内存,没有及时被回收,想要避免就让无用数据不存在引用关系。
  2. 内存膨胀:即在短时间内内存占用极速上升到达一个峰值,想要避免需要使用技术手段减少对内存的占用。
  3. 频繁GC: GC执行的特别频繁,一般出现在频繁使用大的临时变量导致新生代空间被装满的速度极快,而每次新生代装满时就会触发 GC,频繁 GC 同样会导致页面卡顿,想要避免的话就不要搞太多的临时变量,因为临时变量不用了就会被回收。

泄露类型

  1. 闭包
  2. 隐式全局变量
  3. DOM引用
  4. 定时器
  5. 事件监听
  6. 事件监听发布
  7. Map、Set强引用
  8. 未清理的console

深浅拷贝

浅拷贝

&emsp;&emsp;会在栈中开辟另一块空间,并将被拷贝对象的栈内存数据完全拷贝到该块空间中,即基本数据类型的值会被完全拷贝,而引用类型的值则是拷贝了“指向堆内存的地址”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function shallowCopy(target) {
// 引用类型需要开辟一个新的存储地址
if (typeof target === 'object' && target !== null) {
const copy = Array.isArray(target) ? [] : {}
for (const prop in target) {
if (target.hasOwnProperty(prop)) {
copy[prop] = target[prop]
}
}
return copy
}
// 如果是基础类型就直接返回
return target
}

深拷贝

&emsp;&emsp;不仅会在栈中开辟另一块空间,若被拷贝对象中有引用类型,则还会在堆内存中开辟另一块空间存储引用类型的真实数据。

1
JSON.parse(JSON.stringify(obj))
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
const obj3 = {
func: function() {console.log(1)},
obj: { name: 'h' , data: { fn: function() { console.log('data') }, child: 'child' }},
arr: [1,2,3],
und: undefined,
ref: /^123$/,
date: new Date(),
NaN: NaN,
infinity: Infinity,
sym: Symbol(1)
}
function deepCopy(obj, hash = new WeakMap) {
// 日期类型直接返回一个新的日期对象
if (obj instanceof Date) return new Date(obj)
// 正则对象直接返回新的正则对象
if (obj instanceof RegExp) return new RegExp(obj)
// 循环引用使用 weakMap 解决
if (hash.has(obj)) return hash.get(obj)
const desc = Object.getOwnPropertyDescriptors(obj)
// 获取原型上的方法和对象的描述信息,创建新的对象
const copyObj = Object.create(Object.getPrototypeOf(obj), desc)
hash.set(obj, copyObj)

// 循环递归遍历内容,防止还会有共计内存的问题
for (const key of Reflect.ownKeys(obj)) {
let item = obj[key]
if (typeof item === 'object' && item !== null && typeof item !== 'function') {
copyObj[key] = deepCopy(item)
} else {
copyObj[key] = item
}
}
return copyObj
}
console.log(deepCopy(obj3))

深拷贝和浅拷贝的区分

&emsp;&emsp;简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短;如果B没变,那就是深拷贝,自食其力。

节流和防抖

&emsp;&emsp;在进行窗口的resize、scroll、输出框内容校验等操纵的时候,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常之差。那么为了前端性能的优化也为了用户更好的体验,就可以采用防抖(debounce)和节流(throttle)的方式来到达这种效果,减少调用的频率。

防抖

&emsp;&emsp;一定在事件触发 n 秒后才执行,如果在一个事件触发的 n 秒内又触发了这个事件,以新的事件的时间为准,n 秒后才执行,等触发事件 n 秒内不再触发事件才执行。 即计最后一次触发

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
function debounce(func, wait, immediate) {
var timeout, result;
function debounced () {
var context = this, args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function() {
result = func.apply(context, args)
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(function() {
result = func.apply(context, args)
}, wait);
}
return result;
}
debounced.cancel = function(){
cleatTimeout(timeout);
timeout = null;
}
return debounced;
}

节流

&emsp;&emsp;当持续触发事件时,保证一定时间段内只调用一次事件处理函数。即计第一次触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function throttle(func, wait) {

var timer = null;
var startTime = Date.now();

return function(){
var curTime = Date.now();
var remaining = wait-(curTime-startTime);
var context = this;
var args = arguments;

clearTimeout(timer);

if(remaining<=0){
func.apply(context, args);

startTime = Date.now();

}else{
timer = setTimeout(fun, remaining); // 如果小于wait 保证在差值时间后执行
}
}
}

Proxy代理

&emsp;&emsp;Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

1
2
3
// target:要使用 Proxy 包装的目标对象
// handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时,代理 p 的行为
const p = new Proxy(target, handler)

&emsp;&emsp;Proxy可以帮我们对数据的处理,对构造函数的处理,对数据的验证,说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。

Ajax

定义

&emsp;&emsp;Ajax 异步通信技术,是Asynchronous JavaScript+XML的简写,这种技术允许向服务器请求额外的数据而无需卸载(刷新)页面。

通信步骤

  1. 新建XMLHttpRequest对象
  2. 打开要发送的地址通道
  3. 给XMLHttpRequest对象增加侦听器,用来侦听各种情况
  4. 发送数据给打开的地址

数组方法

  1. join():用指定的分隔符将数组每一项拼接为字符串
  2. push():向数组的末尾添加新元素
  3. pop():删除数组的最后一项
  4. unshift():向数组首位添加新元素
  5. shift():删除数组的第一项
  6. slice():按照条件查找出其中的部分元素
  7. splice():对数组进行增删改
  8. filter():过滤功能
  9. concat():用于连接两个或多个数组
  10. indexOf():检测当前值在数组中第一次出现的位置索引
  11. lastIndexOf():检测当前值在数组中最后一次出现的位置索引
  12. every():判断数组中每一项都是否满足条件
  13. some():判断数组中是否存在满足条件的项
  14. includes():判断一个数组是否包含一个指定的值
  15. sort():对数组的元素进行排序
  16. reverse():对数组进行倒序
  17. forEach():es5及以下循环遍历数组每一项
  18. map():es6循环遍历数组每一项
  19. find():返回匹配的项
  20. findIndex():返回匹配位置的索引
  21. reduce():从数组的第一项开始遍历到最后一项,返回一个最终的值
  22. reduceRight():从数组的最后一项开始遍历到第一项,返回一个最终的值
  23. toLocaleString()、toString():将数组转换为字符串
  24. entries()、keys()、values():遍历数组

ES6新特性

let const

  1. let:使用let声明的变量具有块级作用域:,只能在当前声明的代码块中使用,必须要先声明再使用
  2. const:具有块级作用域的特点,只能在当前声明的代码块中使用,必须要先声明再使用。常量,声明时必须赋值,不能改

箭头函数

  1. 可以将箭头函数赋值给一个变量,变量名字就是函数的名字,通过变量名字调用函数。
  2. 如果函数体中只有一行代码,且代码的执行结果就是返回值,可以省略大括号。
  3. 如果形参只有一个,可以省略小括号(如果没有形参,小括号不能省略)

解构赋值

新增基本数据类型 Sybmol

模块化 import导入 export导出

promise

新增类class关键字