0%

前端面试题-js(补充中...)


Var、 let 、const 区别

1
2
3
4
5
6
7
8
9
10
相同点:
varletconst三者都可以声明变量
区别:
var 存在变量提升 而letconst 不存在变量提升

var 定义的变量可以声明多次,而letconst定义的变量只能声明一次

varlet声明的变量可以再次赋值,而const声明的变量不能再次赋值

var 声明的变量没有自身的作用域(var没有块级作用域),而letconst声明的变量有自身的作用域 (在函数内var声明的变量,在函数外也可以调用,但letconst声明的变量有自身的作用域,在函数内定义的变量只能在函数内使用)

ES6声明变量的六种方法

1
2
ES5: var function
ES6: let const import class(class)

函数声明与函数表达式的区别

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
//函数声明(函数名是必须的)
函数声明的最重要的一个特征是函数声明提升,意思是在执行代码之前会先读取函数声明,所以函数声明可以放在调用函数语句之后(类似于变量提升)

sayHello();

function sayHello(){
console.log("Hello");
}

//函数表达式(函数名可写,可不写)
这种形式看起来像常规的变量赋值,先创建一个匿名函数,然后赋值给变量sayHi(不存在提升)

sayHi(); //在这里调用会报错
var sayHi = function(){
console.log("Hi");
}
sayHi(); //Hi


//有趣的javascript例子
function f() {
console.log('I am outside!');
}
(function () {
if(false) {
// 重复声明一次函数f
function f() {
console.log('I am inside!');
}
}
f();
}());
在chrome中输出"I am inside",IE11直接报错,firefox低一点的版本输出"I am outside"

chrome输出的结果很明确的反应了用函数声明式声明的函数的特点–函数在声明之前就可以调用.

IE报错显示缺少对象,因为函数声明在了条件里,违背了函数声明式的原则


函数表达式的作用域:

如果函数表达式声明的函数有函数名,那么这个函数名就相当于这个函数的一个局部变量,只能在函数内部调用,举个栗子:
var f = function fact(x) {
if (x <= 1)
return 1;
else
return x*fact(x-1);
};
console.log(fact()); // Uncaught ReferenceError: fact is not defined
fact()在函数内部可以调用,在函数外部调用就会报错:fact未定义.

函数柯里化 参考

1
2
柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术 优点: 1、参数复用 2、提前确认 3、延迟运行

宏任务和微任务到底是什么

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//面试题
console.log('start')

setTimeout(() => {
console.log('setTimeout')
}, 0)

new Promise((resolve) => {
console.log('promise')
resolve()
})
.then(() => {
console.log('then1')
})
.then(() => {
console.log('then2')
})

console.log('end')

//执行顺序
start
promise
end
then1
then2
setTimeout

1)js是单线程的,但是分同步异步\
2)微任务和宏任务皆为异步任务,它们都属于一个队列\
3)宏任务一般是:script、setTimeoutsetInterval、postMessage、MessageChannel、setImmediate(Node.js 环境)\
4)微任务:Promise.then、Object.observe、MutationObserver、process.nextTick(Node.js 环境)\
5)先执行同步再执行异步,异步遇到微任务,先执行微任务,执行完后如果没有微任务,就执行下一个宏任务,如果有微任务,就按顺序一个一个执行微任务

- 宏任务一般是:<script>标签中的运行代码、setTimeoutsetInterval、postMessage、MessageChannel、setImmediate(Node.js 环境)
- 微任务:Promise.then、Object.observe、MutationObserver、process.nextTick(Node.js 环境)

宏任务、微任务是怎么执行的?

执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕

这里容易产生一个**错误的**认识:就是微任务先于宏任务执行。实际上是先执行同步任务然后在执行异步任务,异步任务是分宏任务和微任务两种的

JS是单线程的,那么JS是如何实现并发请求的?
一般情况下,在单线程中,所有的任务需要排队,前一个任务执行完毕之后,才会去执行下一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着,那么JS是如何实现并发请求的呢?或者说JS是如何实现异步请求的呢?答案就是因为有 消息队列 和 事件循环 的存在

消息队列指的是一个先进先出的队列,在这个队列中可以存在各种消息

事件循环指的是主线程重复从消息队列中获取消息、执行的过程

事件循环的基本流程
JS的主线程一般只会做一件事情,就是从消息队列里取出消息,然后执行消息,再取出消息然后再执行,当消息队列为空时,就会等待直到消息队列中有消息的存在,而且主线程只有再将当前的消息执行完成之后,才会去执行下一个消息,这种机制就是事件循环机制

事件循环的例子
请看下面的一段代码,我们来详细介绍下事件循环中代码的执行流程是什么?
console.log('main1');

process.nextTick(function() {
console.log('process.nextTick1');
});

setTimeout(function() {
console.log('setTimeout');
process.nextTick(function() {
console.log('process.nextTick2');
});
}, 0);

new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then');
});

console.log('main2');

正确的执行顺序请看下方的结果:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

详细分析:
事件循环在执行上流程上,首先将全局代码当作一个宏任务,会先执行这个宏任务,执行这个宏任务的时候,会首先执行同步代码,遇到微任务就添加到微任务队列,遇到宏任务就会添加到宏任务队列,当同步代码执行完毕的时候,会开始执行微任务队列中的任务,执行完毕之后会执行宏任务队列中的任务,所以,本题最重要的就是要区分好宏任务是什么,微任务是什么,setTimeout中的回调函数时宏任务,process.nextTick是微任务,所以执行顺序是上面的结果

setTimeout(function(){
console.log('1');
});

new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3');
}).then(function(){
console.log('4')
});

console.log('5');
// 2 5 3 4 1

1.遇到setTimout,异步宏任务,放入宏任务队列中
2.遇到new Promisenew Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
3.Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(‘5’);输出5;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出34,微任务队列为空
6.从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空

setTimeout(()=>{
new Promise(resolve =>{
resolve();
}).then(()=>{
console.log('test');
});
console.log(4);
});

new Promise(resolve => {
resolve();
console.log(1)
}).then( () => {
console.log(3);

Promise.resolve().then(() => {
console.log('before timeout');
}).then(() => {
Promise.resolve().then(() => {
console.log('also before timeout')
})
})
})
console.log(2);

1.遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中;
2.遇到new Promisenew Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1
3.Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(2),输出2;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
6.从微任务队列中取出任务a到主线程中,输出 before timeout;
7.从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
8.从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
9.从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
10.从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空


console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})

process.nextTick(function() {
console.log('6');
})

new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
// 1 7 6 8 2 4 3 5 9 11 10 12

如何判断变量为 NaN( Not a Number,非数字)

NaN不能用相等操作符(== 和 ===) 来判断, NaN === NaN 会返回 false
虽然 NaN 是“Not a Number”,但是它的类型还是数值类型

1
2
3
4
5
//通过isNaN()判断,isNaN()会先将参数转为Number 类型
isNaN(NaN) // 返回true
isNaN(1000) // 返回false
isNaN('小明') // 返回true(判断前会转换成number类型)
isNaN('101') // 返回false

null 和 undefined 区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    总的来说 nullundefined 都代表空, null是一个关键字,表示一个空值的对象引用,undefined表示一个未定义的值,用作默认初始值
//typeof
typeof null // 'object'
typeof undefined // 'undefined'

//
let a = null // 显式地将a赋值给null
let b; // b的默认值为undefined

console.log(a) // 输出 null
console.log(b) // 输出 undefined
//== 与 ===
null == undefined // true
null === undefined // false
!!null === !!undefined // true
//+ 运算 与 Number()
let a = undefined + 1 // NaN
let b = null + 1 // 1
Number(undefined) // NaN
Number(null) // 0

JS实现继承的方式有哪些?

1
构造函数继承 原型链继承 组合继承 寄生组合继承 ES6类(class类)继承

内存中的堆和栈

1
2
3
4
5
6
栈:先进后出,自动分配释放
堆:先进先出,手动释放,容易内存泄漏

基本数据类型:nullundefinedStringNumberBooleanSymbol(ES6) 基本数据类型可以直接访问,
按值进行分配,存放在**栈内存**中的简单数据段
引用型:OBject,存放在**堆内存**中,实际栈保存的是一个指针,这个指针指向另一个位置(**堆内存**)

== 和 ===区别

1
2
3
4
5
相等操作符(==)不会判断数据类型,
let result1 = ("55" == 55); // true

全等运算符(===)会判断数据类型
let result1 = ("55" === 55); // false

如何来区分js数据类型的?比如区分对象和数组

判断数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let arr = ['1']

//使用 toString 方式 (返回[object constructorName]的字符串格式)
console.log( Object.prototype.toString.call(arr) === '[object Array]' ) //true

//使用Array.isArray(arr) 来判断值是否为数组
console.log( Array.isArray( arr ) ) //true

// 使用 instanceof 方式 (用来检测构造函数的prototype 属性是否出现在某个对象的原型链上)
console.log( arr instanceof Array ) //true

//使用 constructor 方式 (constructor是prototype对象的一个属性,指向的是prototype属性所在的构造函数,可以判断数据类型)
console.log( arr.constructor === Array ) //true

//使用 __proto__ 方式(意思为实例的__proto__属性,是否指向构造函数的prototype属性) (PS: .__proto__前后两个下划线)

console.log( arr.__proto__ === Array.prototype ) //true

//使用 isPrototypeOf 方式 (用于测试一个对象是否存在于另一个对象的原型链上)
console.log( Array.prototype.isPrototypeOf( arr ) ) //true

//使用Object.getPrototypeOf 方式 (返回指定对象的原型,内部[[Prototype]]属性的值)
console.log( Object.getPrototypeOf( arr ) === Array.prototype ) //true

判断对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {}

//toString 方式(返回[object constructorName]的字符串格式)
console.log( Object.prototype.toString.call(obj) === '[object object]' ) //true

//使用instanceof关键字来判断(用来检测构造函数的prototype 属性是否出现在某个对象的原型链上)
console.log( obj instanceof Object ) //true

//使用 constructor 来判断;(constructor是prototype对象的一个属性,指向的是prototype属性所在的构造函数,可以判断数据类型)
console.log( obj.constructor === Object ) //true

//使用typeof 来判断(返回对应的数据类型,数组不行)
console.log( typeof obj === 'object' ) //true
1
2
3
4
5
6
7
8
9
10
11
12
13
引用类型判断

区别对象、数组、函数可以使用Object.prototype.toString.call 方法。判断某个对象值属于哪种内置类型。

console.log(Object.prototype.toString.call(123)) // [object Number]
console.log(Object.prototype.toString.call('123')) // [object String]
console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call({})) // [object Object]
console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call(function(){})) // [object Function]
console.log(Object.prototype.toString.call(this)); // [object Window]

前端开发中常用的几种设计模式 参考1参考2

设计模式可以分为三大类:

结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。
创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
行为型模式(Behavioral Patterns):用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性

单例模式——饿汉模式&&懒汉模式

什么是单例模式?
单例模式是一种常见的“设计模式”

单例模式的应用场景
某个类,不应该有多个实例,此时就可以使用单例模式(DataSource就是一个典型的案例,一一个程序中只有一个实例,不应该实例化多个DataSource对象)。如果尝试创建多个实例,编译期就会报错

两种典型的方式实现单例模式

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
//饿汉模式
public class singlePattern {
//先创建一个表示单例的类
//我们就要求Singleton这个类只能有一个实例
//饿汉模式的单例实现
//饿汉模式的单例实现,“饿”指得是,只要类被加载,实例就会立刻创建(实例创建时机比较早)
static class Singleton{
//把 构造方法 变为私有,此时在该类外部,就无法 new 这个类的实例了
private Singleton(){

}
//再来创建一个 static 的成员,表示Singleton 类唯一的实例
//static 和 类相关,和实例无关,类在内存中只有一份,static 成员也就只有一份
static Singleton instance = new Singleton();
//new没报错是因为Singleton类是singlePattern的内部类,singlePattern是可以访问内部类的private成员的
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
//此处得 getInstance 就是获取实例得唯一方式,不应该使用其他方式创建实例了
Singleton s = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s == s2);
}
}

}
只要类被加载,就会立刻实例化Singleton实例,后续无论怎么操作,只要严格使用getInstance,就不会出现其他实例

//懒汉模式
public class lazyPattern {
//使用懒汉模式来实现,Singleton类被加载的时候,不会立刻实例化
//等到第一次使用这个实例的时候,再实例化
static class Singleton{
private static Singleton instance = null;

public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
public static void main(String[] args) {

}
}

类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化

如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”

一般认为“懒汉模式” 比 “饿汉模式”效率更高。

懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销

在数组对象中添加新字段的方法

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
//ES6
//1、Object.assign()
var arr = [
{age:18},
{age:30}
]
// 添加新字段
arr.forEach(item=>{
Object.assign(item,{name:'井空'})
})
console.log(arr)
//或者
arr.forEach((item , i) => {
item['name'] ='井空' // 和item.name='井空'是一样得
})
console.log(arr)
//2、map()
map正常情况下会跟return结合使用,而map的作用就是重新整理数据结构
有时候我们需要对后台传来的数据做一些处理,这时候使用map就可以得到想要的数据
//2.1、
let arr =[
{price: '25'},
{price: '15'}
];
let _arr = [];
arr.map((item, index) => {
//下面这行代码同样适合放在上面的forEach里面
_arr.push(Object.assign({},item,{flag: false})) //注意这个‘{}’,添加时原数组arr不改变,去掉之后arr数组跟_arr数组数据一样,改变了
})
console.log(arr) // [{price: '25'},{price: '15'}]
console.log(_arr) // [{price: '25',flag: false},{price: '15',flag: false}]

//2.2、
let ar =[
{price: '25'},
{price: '15'}
];
let as=ar.map(item => {
//解构item,且给item添加新的值 flag,键值(flag: false)
//这里解构相当于把item里面的值都拿出来重新赋值
return { ...item, flag: false } //forEach不能return,这句不适合
})
console.log(ar,'ar') // [{price: '25'},{price: '15'}]
console.log(as,'as') // [{price: '25',flag: false},{price: '15',flag: false}]
// 原数组ar不改变

forEach用什么方法结束

正常终止for循环,我们可以使用break关键字来实现;

forEach循环,不能使用break和continue这两个关键字;

因为这两个关键字要在循环中使用,而forEach中所执行的是callback,callback是个函数所以不能使用;

使用 return 的话,只能跳出本次循环执行下一次循环,并不会终止forEach循环;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//运用抛出异常(try catch):throw new Error('error message');
try {
this.txt.forEach((item, index) => {
if (!item.name) {
throw new Error('单课不能为空!')
}
if (!item.val) {
throw new Error('上课老师不能为空!')
}
if (!item.date) {
throw new Error('上架时间不能为空!')
}
})
} catch (e) {
this.$message.warning(e.message)
return
};

伪数组 转化为真数组

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn() {  
console.log(arguments); //Arguments显示的也有方括号 [1,2,3,4,5...] ,但是后面多了一些其他方法;也有length属性,但没有数组的push,pop等那些方法,像数组又不是数组 ,[[prototype]]可以看到arguments伪数组的原型指向的是Object对象, 真数组的__proto__指向的是Array数组

//ES6语法 拓展运算符
let newArr=[...arguments]
//ES6的Array.from
let newArr=Array.from(arguments)
//利用Array的原型对象的slice方法,配合call()方法修改slice中this指向
//slice原本是数组的截取子数组的方法,这里给数组的原型对象方法slice的指向强制改成arguments
let newArr= Array.prototype.slice.call(arguments)
}
fn(1,2,3,4,5)

扩展符”…”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//在对象中的应用
//合并对象
let obj1 = {a:1};
let obj2 = {b:2};
let obj3 ={...obj1,...obj2)};
console.log(obj3) // {a:1,b:2}

//给对象赋默认值
var obj_1 = {a: 1,b: 2}
var obj_2 = {
...obj_1,
b: 3
}
console.log(obj_2); {a:1,b:3} //合并的对象中有相同的属性会覆盖

//遍历对象
var obj = {name: 'name',age: 18,sex: '男'}
console.log({ ...obj}); //{name: 'name',age: 18,sex: '男'}

?? / ?. / || / !! /

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

// ?.应用场景,多级嵌套

const arr_obj = [{
id: '1',
name: 'product',
type: 'product',
children: [
{id: '3',name: 'subitem1',type: 'subitem',
children: [],
},
{id: '4',name: 'item2',type: 'item',
children: [
{id: '5',name: 'subitem3',type: 'subitem',children: [],}
],
},
{id: '5',name: 'item3',type: 'item',
children: [
{id: '6',name: 'subitem7',type: 'subitem',
children[]
},
],
},
{id: '2',name: 'group',type: 'group',
children: [
{id: '10',name: 'product1',type: 'product',
children: [
{id: '11',name: 'subitem1',type: 'subitem',
children: [],
},
{id: '12',name: 'item3',type: 'item',
children: [
{name: 'subitem5',id: '18',type: 'subitem',}
],
}
]
}]
}
]
}]
// 现在从上面的数组中,检查每个对象的类型,
// 如果类型是product或group,添加property disabled true,
// 如果不是添加property disabled false
const handleLoop = (array) => array.map((elem) => {
return {
...elem,
...(elem.children && {children: handleLoop(elem.children)}), //递归
disabled: elem ?.type === "product" || elem ?.type === "group" ? true : false
}
})
console.log(handleLoop(arr_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
map 循环遍历数组、返回一个新的数组

forEach 循环遍历数组,不改变原数组, forEach不能return

push/pop 在数组的末尾添加/删除元素 改变原数组

unshift/ shift 在数组的头部添加/删除元素,改变原数组

join 把数组转化为字符串

some 有一项返回为true,则整体为true,类似于indexOf 只要数组某个元素符合条件,则返回true

every 有一项返回为true,则整体为false

filter 数组过滤,通常也是跟return使用,当回调函数返回 true 就留下来

slice(start, end) 数组截取,包括开头,不包括截取,返回一个新的数组

splice(start, number, value) 删除数组元素,改变原数组

indexof/lastindexof: 查找数组项,返回对应的下标

concat:数组的拼接,不影响原数组,浅拷贝

sort:数组排序 改变原数组

reverse: 数组反转,改变原数组

数组常用方法

1
2
3
push()    添加末尾, 并返回新的数组长度。"原数组改变"
unshift() 开头添加, 并返回新的数组长度。"原数组改变"
concat(arr1,arr2, ...) 合并两个或多个数组,生成一个新的数组。’原数组不变‘

1
2
3
4
pop()   删除并"返回"数组的最后一个元素,若该数组为空,则返回undefined"原数组改变"
shift() 删除数组的第一项,并"返回"第一个元素的值,若该数组为空,则返回undefined"原数组改变"
splice() 传入两个参数,分别是开始位置,删除元素的数量,"返回"包含删除元素的数组。 "原数组改变"
slice() 按照条件查找出其中的部分内容,’原数组不变‘

1
2
3
即修改原来数组的内容,常用splice

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

1
2
3
4
5
即查找元素,返回元素坐标或者元素值

indexOf() 返回要查找的元素在数组中的位置,如果没找到则返回 -1,’原数组不变‘
includes() 返回要查找的元素在数组中的位置,找到返回true,否则false,’原数组不变‘
find() 返回第一个匹配的元素

分割

1
join() 将数组的每一项用指定字符连接形成一个字符串。默认连接字符为 “,” 逗号

迭代方法

1
2
3
4
5
6
7
常用来迭代数组的方法(都不改变原数组)有如下:

some() 对数组中的每一项进行判断,若都不符合则返回false,否则返回true,(如果有一项函数返回 true ,则这个方法返回 true)
every() 对数组中的每一项进行判断,若都符合则返回true,否则返回false
forEach() 用于调用数组的每个元素,并将元素传递给回调函数。’原数组不变‘
filter() 过滤数组中,符合条件的元素并返回一个新的数组
map() 原数组的每一项执行函数后,返回一个新的数组 ’原数组不变‘

reduce

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
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
语法:
arr.reduce( function ( prev, cur, index, arr ){
一系列操作
}, init);
//箭头写法
arr.reduce( ( prev, cur, index, arr ) => {
一系列操作
}, init);

prev: 必需(初始值, 或者计算结束后的返回值);
cur: 必需(当前元素);
index: 可选(当前元素的索引);
arr:可选(当前元素所属的数组对象);
init: 可选(传递给函数的初始值);

场景一:数组累加、累乘

let arr1 = [1,2,3,4,5]
console.log(arr1.reduce((x,y)=>x+y));// 15
console.log(arr1.reduce((x,y)=>x*y));// 120

场景二:计算数组中每个元素出现的次数

let arr2 = ['a','b','c','d','a','b','c','a','b','a']
let num = arr2.reduce((prev,cur)=>{
if(cur in prev){//如果prev(初始对象)包含cur(当前元素),数量累加
prev[cur]++
}else{
prev[cur] = 1
}
return prev
},{});//初始值需要设置一个空的对象
console.log(num);// {a: 4, b: 3, c: 2, d: 1}

场景三:数组去重

let arr3 = [1,2,3,4,3,2,1,2,3,1]
let remo = arr3.reduce((prev,cur)=>{
if(prev.indexOf(cur)==-1){//如果prev没找到cur
return prev.concat(cur)
}else{
return prev
}
// if(!prev.includes(cur)){//如果prev不包含cur
// return prev.concat(cur)
// }else{
// return prev
// }
},[]);// 初始值设置一个空数组
console.log(remo);// [1,2,3,4]

场景四:将二维数组转化为一维数组

let arr4 = [[0, 1], [2, 3], [4, 5]]
let newArr1 = arr4.reduce((prev,cur)=>{
return prev.concat(cur)
},[])
console.log(newArr1); // [0, 1, 2, 3, 4, 5]

场景五:将多维数组转化为一维数组

let arr5 = [[0, 1], [2, 3], [4,[5,6,7,8]]]
function newArr(arr5){
return arr5.reduce((prev,cur)=>prev.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr5)); //[0, 1, 2, 3, 4, 5, 6, 7, 8]

场景六:求数组中最大的值

let arr7 = [1,5,9,4,3,7,12]
let maxNum = arr7.reduce((prev,cur)=>{
return Math.max(prev,cur);// Math.max方法可以求出给定参数中最大的数,Math.min方法可以求出给定参数中最小的数
})
console.log(maxNum);// 12

冒泡排序
冒泡排序(Bubble Sort) 一种交换排序方法,通过比较两两相邻的元素,按照升序或者降序的规则进行位置的替换,需要使用到双层循环遍历,每遍历一圈只会对对一个数值进行排序,总共需要遍历n-1次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//冒泡排序
/* 1. 从当前元素起,向后依次比较每一对相邻元素,若逆序则交换 */
/* 2. 对所有元素均重复以上步骤,直至最后一个元素 */
/* int* a: 排序目标数组; int n: 元素个数 */
void BubbleSort(int* a, int n){
for (int i=0; i < n - 1; i++){//n个数比较n-1次,总共比较n-1次

for (int j=0; j < n - i - 1; j++){//每次比较n-i-1的左右大小判断是否交换

if (a[j] > a[j + 1]){

int temp = a[j];
a[j] = a[j+1];
a[j + 1] = temp;
}
}
}
}

快速排序
通过一趟排序将待排序列以枢轴为标准划分成两部分,使其中一部分记录的关键字均比另一部分小,再分别对这两部分进行快速排序,以达到整个序列有序

第一步:找基准,采用二分的方式,从数组中找一个基数元素(一般就取第一个元素为基数)将一串数据分为两串;

第二步:分区,重新排列数组,将小于基数的元素放左边,大于基数的元素放右边,相同的值放任意位置,使得基数成为中间元素;

第三步:递归,将两个组的元素分别进行排序

1
2
3
4
//快速排序
//一趟快速排序的描述,取一个枢纽,排序好比它小的左边,大的右边,并且返回这个值的指针

......

倒序

1
reverse() 将数组倒序。"原数组改变"

数组排序

1
2
3
4
5
6
7
8
9
sort(首元素地址(必填), 尾元素地址的下一个地址(必填), 比较函数(非必填));
如果直接sort(数组名),则从小到大排序(即升序),以下为倒叙

var arr = [30,10,111,35,50,45];
arr.sort(function(a,b){
return b - a; //从大到小
// return a - b; //从小到大
})
console.log(arr);//输出 [111, 50, 45, 35, 30, 10]

数组去重

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
1)利用ES6 Set去重(ES6中最常用)
function unique (arr) {
return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

2)利用for嵌套for,然后splice去重(ES5中最常用)
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1);
j--;
}
}
}
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}] //NaN和{}没有去重,两个null直接消失了

3)利用indexOf去重
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array .indexOf(arr[i]) === -1) {
array .push(arr[i])
}
}
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重

4)利用includes
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array =[];
for(var i = 0; i < arr.length; i++) {
if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}] //{}没有去重

//箭头写法

1let uniqueOne = Array.from(new Set(arr)) console.log(uniqueOne)


2) let uniqueTwo = arr => {
let map = new Map(); //或者用空对象 let obj = {} 利用对象属性不能重复得特性
let brr = []
arr.forEach( item => {
if(!map.has(item)) { //如果是对象得话就判断 !obj[item]
map.set(item,true) //如果是对象得话就obj[item] =true 其他一样
brr.push(item)
}
})
return brr
}
console.log(uniqueTwo(arr))


3let uniqueThree = arr => {
let brr = []
arr.forEach(item => {
// 使用indexOf 返回数组是否包含某个值 没有就返回-1 有就返回下标
if(brr.indexOf(item) === -1) brr.push(item)
// 或者使用includes 返回数组是否包含某个值 没有就返回false 有就返回true
if(!brr.includes(item)) brr.push(item)
})
return brr
}
console.log(uniqueThree(arr))

4let uniqueFour = arr => {
// 使用 filter 返回符合条件的集合
let brr = arr.filter((item,index) => {
return arr.indexOf(item) === index
})
return brr
}
console.log(uniqueFour(arr))

bind、call、apply 区别

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
相同点:
1.都是用来改变函数的this对象的指向的。
2.第一个参数都是this要指向的对象。
3.都可以利用后续参数传参

区别:
1.fn.call(obj, arg1, arg2, …),调用一个函数, 第一个参数为要改变的this,第二个参数为要传递的参数,参数与方法中是一一对应的。
2.fn.apply(obj, [argsArray]),第一个参数为要改变的this,第二个参数为要传递的参数作为一个数组(或类数组对象)提供的参数
3.bind 和call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是bind 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数

var xw = {
name : "小王",
gender : "男",
age : 24,
say : function() {
alert(this.name + " , " + this.gender + " ,今年" + this.age);
}
}
var xh = {
name : "小红",
gender : "女",
age : 18
}
xw.say(); //小王 , 男 , 今年24

//如何用xw的say方法来显示xh的数据
xw.say.call(xh);

xw.say.apply(xh);

xw.say.bind(xh)();
//如果直接写xw.say.bind(xh)是不会有任何结果的,call和apply都是对函数的直接调用,而bind方法返回的仍然是一个函数,因此后面还需要()来进行调用才可以

//------------------------------------------------------------
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function(school,grade) {
alert(this.name + " , " + this.gender + " ,今年" + this.age + " ,在" + school + "上" + grade);
}
}
var xh = {
name : "小红",
gender : "女",
age : 18
}
//可以看到say方法多了两个参数,我们通过call/apply的参数进行传参
xw.say.call(xh,"实验小学","六年级");

xw.say.apply(xh,["实验小学","六年级"]);
//call后面的参数与say方法中是一一对应的,而apply的第二个参数是一个数组,数组中的元素是和say方法中一一对应,这就是两者最大的区别
//但是由于bind返回的仍然是一个函数,所以我们还可以在调用的时候再进行传参
xw.say.bind(xh)("实验小学","六年级");

本地存储的方式有哪些?区别及应用场景?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javaScript本地缓存的方法我们主要讲述以下四种:

cookie
sessionStorage
localStorage
indexedDB
区别
关于cookie、sessionStorage、localStorage三者的区别主要如下:

存储大小:cookie数据大小不能超过4k,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
数据与服务器之间的交互方式,cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端; sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存
应用场景
在了解了上述的前端的缓存方式后,我们可以看看针对不对场景的使用选择:

标记用户与跟踪用户行为的情况,推荐使用cookie
适合长期保存在本地的数据(令牌),推荐使用localStorage
敏感账号一次性登录,推荐使用sessionStorage
存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexedDB

闭包?闭包使用场景 和 什么是立即执行函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
理解: 闭包就是函数中包含另一个函数,内部函数可以引用外部函数的参数和变量,参数和变量不会被垃圾回收机制收回
function fn(a){
return function(){
//访问道这个a
console.log(a);
}
}
fn('hello'); //调用外部的函数
fn('hello')() //调用内部函数

闭包: 定义在一个函数内部的函数(方法里面返回方法)。
闭包的使用场景:settimeout、回调、函数防抖、封装私有变量

过多使用会导致内存泄漏的问题


立即执行函数:声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;
立即执行函数的作用:
1.不必为函数命名,避免了污染全局变量。
2.立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
3.封装变量。

深拷贝浅拷贝的区别?

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
(理解:b拷贝了a,如果b变了,a跟着变,那就是浅拷贝;b变了,a没变,那就是深拷贝)

浅拷贝
如果当数组或对象中的值是`基本类型数据`,那拷贝后的数据和原数据是完全没有关联,且互不影响的两个数据,
如果数组或对象的值是`引用类型数据`的话,拷贝后的数组或对象中的引用类型的值跟原数据中的引用类型的值,还是会保持共同的内存地址

在JavaScript中,存在浅拷贝的现象有:

1Object.assign

// ---当对象为基本类型数据
let obj1 = {
sex: '男'
age: '14'
};
let obj2 = Object.assign({}, obj1);
obj2.age = '23';
obj2.sex = '女';
console.log(obj1); // { sex: '男', age: '14' }
console.log(obj2); // { sex: '女', age: '23' }

//---当对象引用类型数据

let obj3 = {
person: {name: "完美"},
age: '14'
};
let obj4 = Object.assign({}, obj3);
obj4.person.name = "哈哈哈";
obj4.age = '23'
console.log(obj3); // { person: { name: '哈哈哈'}, age: '14' }
console.log(obj4); // { person: { name: '哈哈哈'}, age: '23' }


2Array.prototype.slice()

//---当数组基本类型数据
let a1 = [1, 2];
let a2 = a1.slice();
a2[0] = 3;
console.log(a1);// [1, 2]
console.log(a2);// [3, 2]

//---当数组有引用类型数据
let arr = [1, 2, {name: '完美'}];
let arr2 = arr.slice();
arr2[2].name = '哈哈哈哈';
arr2[0] = 3;
console.log(arr); //[ 1, 2, { name: '哈哈哈哈' } ]
console.log(arr2); //[ 3, 2, { name: '哈哈哈哈' } ]



3Array.prototype.concat()

//---当数组为基本类型数据
let a1 = [1, 2];
let a2 = a1.concat();
a2[0] = 3;
console.log(a1);// [1, 2]
console.log(a2);// [3, 2]

//---当数组中有引用类型数据
let arr = [1, 2, {name: '完美'}];
let arr2 = arr.concat();
arr2[2].name = '哈哈哈哈';
arr2[0] = 3;
console.log(arr); //[ 1, 2, { name: '哈哈哈哈' } ]
console.log(arr2); //[ 3, 2, { name: '哈哈哈哈' } ]

4、扩展运算符(...)

//---当对象为基本类型数据
var obj = { name: 'FungLeo', sex: 'man', old: '18'}
var { ...obj2 } = obj
obj.old = '22'
console.log(obj) // { name: 'FungLeo', sex: 'man', old: '18'}
console.log(obj2)// { name: 'FungLeo', sex: 'man', old: '22'}

//---当对象中有引用类型数据
var obj = { name: 'FungLeo', sex: 'man', old: '18',id:{idx: 1}}
var { ...obj2 } = obj
obj.old = '22'
obj.id.idx = '0'
console.log(obj) // { name: 'FungLeo', sex: 'man', old: '18',id:{idx: 0}}
console.log(obj2)// { name: 'FungLeo', sex: 'man', old: '22',id:{idx: 0}}

深拷贝,就是不管原数据中值是什么类型的数据,拷贝后的新数据跟原数据是完全没有关联的

常见的深拷贝方式有:

//万能转换器JSON.parse(JSON.stringify(obj)) 深拷贝已对象,它可以`深拷贝多层级的,不同担心嵌套问题`。
1JSON.parse(JSON.stringify()),//这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
例如:
let arr = [1,2, {name: 'gg'}];
let arr1 = JSON.parse(JSON.stringify( arr ));
arr1[2].name = 'hh';
arr1[0] = 5;
console.log(arr) //[1,2 {name: 'gg'}]
console.log(arr1) //[5,2 {name: 'hh'}]


2、jQuery.extend()

// 注意要引入jQuery
let obj1 = {
name: "张三",
age: 18,
father: { age: 45},
fn: function (n) {
return n
}
}

let obj2 = $.extend(true, {}, obj1)
obj2.name = '李四';
obj2.father.age = 50;
console.log(obj1,obj1.fn(9)) // {name: "张三",age: 18,father: { age: 45},fn:f()},9
console.log(obj2,obj2.fn(99)) // {name: "李四",age: 18,father: { age: 50},fn:f()},99


3、利用递归遍历对象或数组

function clone(source){
var result;
if(Object.prototype.toString.call(source)==='[object Object]'){
result = {};
}else if(Object.prototype.toString.call(source)==='[object Array]'){
result = []
}else{
return;
}
for(var attr in source){
if(Object.prototype.toString.call(source[attr])==='[object Array]' || Object.prototype.toString.call(source[attr])==='[object Object]'){
result[attr] = clone(source[attr])
}else{
result[attr] = source[attr];
}
}
return result;
}

使用

var arr = [1,2,{
name:"张三",
age:12,
wife:{
name:"翠花",
age:11
},
fn: function (n) {
return n
}
}];
var res = clone(arr);
res[0] = 5;
res[2].name = '李四'
res[2].wife.name = '赵五';
console.log(arr,arr[2].fn(9)); // [1,2,{name:"张三",age:12,wife:{name:"翠花",age:11}}],9
console.log(res,res[2].fn(99)); // [5,2,{name:"李四",age:12,wife:{name:"赵五",age:11}}],99


_.cloneDeep() //函数库lodash,该函数库也有提供_.cloneDeep用来做深拷贝

JavaScript中的数据类型

1
2
3
string、number、Booleanundefinednull、object、ArraySymbol


防抖和节流

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
理解: 电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

在游戏回城的时候被打断,再次点回城就会重新计时,最终只有没被打断的最后一次,才能成功回城,就是防抖


电梯第一个人进来后,15秒后准时运送一次,这是节流

鼠标点击下一张轮播图,不管单位时间内连续点击了多少次,轮播图都是2秒换下一张,就是节流


1、防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时(在单位时间内频繁触发事件,只有最后一次生效)

应用场景: 文本输入的验证;
`<input type="text" id='debounce' onInput='debounceInput(event)' >`

function debounce(fn, delay) {
//定义一个定时器,保存上一次的定时器
let time = null
return function (...args) {
// let args = arguments;
//取消上一次定时器
if (timer) {
clearTimeout(timer);
}
//延迟执行
timer = setTimeout(()=> {
//外部传入的真正要执行的函数
fn.apply(this, args);
}, delay);
};
}
//input事件
function onInput(e){
val = e.target.value
if(val){
console.log('有防抖',val)
}
}
//使用
const debounceInput = debounce(onInput, 300)

2、节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效(在单位时间内频繁触发事件,只生效一次(也就是只有第一次生效))

应用场景:滚动加载,加载更多或滚到底部监听。

// 尾节流,定时器实现,不会立即执行,而是在delay之后执行
// 最后停止触发之后,还会执行一次
function throttle(fn, delay) {
let timer = null;
return function (..args) {
// let args = arguments;
if (timer) {
return;
}
timer = setTimeout( () => {
fn.apply(this, args);
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)


// 定时器不用箭头函数时this要改变一下
// let _this = this;
// //let args = arguments;
// if (timer) {
// return;
// }
// timer = setTimeout(function () {
// fn.apply(_this, args);
// timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
// }, delay)
}
}// 尾节流,定时器实现,不会立即执行,而是在delay之后执行
// 最后停止触发之后,还会执行一次
function throttle(fn, delay) {
let timer = null;
return function (..args) {
// let args = arguments;
if (timer) {
return;
}
timer = setTimeout( () => {
fn.apply(this, args);
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)


// 定时器不用箭头函数时this要改变一下
// let _this = this;
// //let args = arguments;
// if (timer) {
// return;
// }
// timer = setTimeout(function () {
// fn.apply(_this, args);
// timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
// }, delay)
}
}
//事件
function handle(){
console.log(new Date())
}
//使用
const throttleHandler = throttle(handle, 3000)
window.addEventListener('scroll', throttleHandler)

箭头函数和普通函数有什么区别

1
2
3
4
5
6
(1). 箭头函数更加简洁;
(2). 箭头函数不会创建自己的this,所以它没有自己的this,它只会在自己作用域的上一层继承this,所以箭头函数中的this指向在它定义时就确认了,之后不会再改变,所以箭头函数的this值永远不会改变;
(3). call()、apply()、bind()等方法不能改变箭头函数this的的指向;
(4). 箭头函数不能作为构造函数使用;
(5). 箭头函数没有自己的arguments
(6). 箭头函数没有prototype(原型),原型是undefined

你是怎么理解面向对象的,什么是面向对象,用面向对象做过什么

1
2
3
4
5
6
7
8
9
10
11
12
13
面向对象的三大特性:

封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。

继承
提高代码复用性;继承是多态的前提。

多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

为什么要用面向对象?
易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

面向对象和面向过程的区别

1
2
3
4
5
6
7
8
9
10
11
面向对象以对象为中心。先把要完成的功能封装成一个一个的对象,通过调用对象的方法或属性来完成功能

优点:不仅关注眼前的事件实现,也关注未来可能发生的事件。具有高度的拓展性和复用性,特点是继承、封装、多肽

缺点:如果只是单一的功能实现,面向对象的设计思路会过于繁琐

面向过程是以事件为中心,按照我们编写的代码是根据完成一个步骤的过程来进行

优点:根据事情的目的分解出过程,再一步步实施。对于不复杂的事件执行效率快

缺点:只关注眼前事件的实现

如何解决数字精度丢失的问题?

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

理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下:

parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True
封装成方法就是:

function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}

对于运算类操作,如 +-*/,就不能使用 toPrecision 了。正确的做法是把小数转成整数后再运算。

主要思想是:将小数先转换成拆分两个字符串,然后计算小数部分的字符串的长度,然后利用这个长度将小数变成整数!
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
最后还可以使用第三方库,如Math.js、BigDecimal.js

其他 +-*/ 详见 解决数字精度丢失

JavaScript 中内存泄漏的几种情况?

1
2
3
4
(1) 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收
(2) 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
(3) 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
(4) 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中

TypeScript 和JavaScript有什么区别

1
ts是js的超集,是js的扩展语言,结合ide可以让我们再开发过程中知道变量的类型并提供联想提示

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
原始模型(DOM0) 
只支持冒泡,不支持捕获
同一类型的事件只能绑定一次
绑定监听函数
HTML代码中直接绑定
<input type="button" οnclick="fun()">
通过js代码绑定
var btn = document.getElementById(".btn");
btn.onclick = fun;
IE模型(基本不用)
事件处理阶段
事件冒泡阶段
事件绑定监听函数
attachEvent()
detachEvent()

标准模型(dom2模型)
事件捕获阶段
事件处理阶段
事件冒泡阶段

事件绑定监听函数
addEventListener()
removeEventListener()

ES6声明变量的六种方法

1
2
ES5: var function
ES6: let const import class(class)

JS实现继承的方式有哪些?

1
构造函数继承 原型链继承 组合继承 寄生组合继承 ES6类(class类)继承

原型,原型链 ?

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
125
126
先了解构造函数
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() { alert(this.name) }
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

上面的例子中 person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person 即:
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true

我们要记住两个概念(构造函数,实例):
person1 和 person2 都是 构造函数 Person 的实例
一个公式: 实例的构造函数属性(constructor)指向构造函数

1、原型 prototype
常规的数组 [ ] 和对象 { } 是没有原型的,原型是函数function特有的;
每一个函数都会有prototype属性,被称为显式原型;
每一个实例对象都会有__proto__属性,其被称为隐式原型
function Student(){

}
var stu = new Student();
Student.prototype.name = 'Jerry';/*如果prototype中 有这个属性,在这个语句后也会生成相应属性*/
console.log(stu.name);/*输出Jerry*/
console.log(stu.__proto__ === Student.prototype) // true

同样的,不仅仅是属性,方法也是可以继承的

function Student(){

}
var stu = new Student();
Student.prototype.sayHello = function (){
console.log('hello');
};
stu.sayHello();/*调用方法后输出hello*/

但是如果当我们在构造函数中拥有和原型一样的属性或者方法的时候,会优先使用构造函数的属性和方法

function Student(name, age, sex){
this.name = name;
this.age = age;
this.sex = sex;
this.sayHello = function (){
console.log("hello I'm", name);
}
}
var stu = new Student('Tom', 18, 'male');
Student.prototype.sayHello = function (){
console.log('hello');
};
stu.sayHello();/*调用sayHello方法*/
console.log(stu.name);/*输出名字*/

//输出结果
//hello I'm Tom
//Tom

利用原型的这个特征,我们就可以把一些对象的共有属性提取出来

Student.prototype = {
school : '清华大学',
country : '中国'
}
function Student(name, age, sex){
this.name = name;
this.age = age;
this.sex = sex;
}
var stu1 = new Student('Tom', 18, 'male');
var stu2 = new Student('Jerry', 20, 'female');
console.log(stu1.school, stu1.country, stu1.name, stu1.age, stu1.sex);//清华大学 中国 Tom 18 male
console.log(stu2.school, stu2.country, stu2.name, stu2.age, stu2.sex);//清华大学 中国 Jerry 20 female

constructor
  在原型的使用中,我们还会遇到一个属性,constructor,它是用于查看对象的构造器的属性,并且这个属性也是系统自带的,我们也可以对其进行修改:
function Student(){
}
var stu = new Student();
console.log(stu.constructor); //Student(){}

修改
function Student(){
}
var stu = new Student();
stu.constructor = function Teacher(){};
console.log(stu.constructor);//Teacher(){}


2、原型链
原型链可以理解成一个构造器连接着上一层的实例,上一层的实例接着往上连接,以此类推,就形成了原型链,具体的示例如下

function Grand(){
this.getGrand = 'grand';
}
var grand = new Grand();
Father.prototype = grand;
function Father(){
this.getFather = 'father';
}
var father = new Father();
Son.prototype = father;
function Son(){
this.getSon = 'son';
}
var son = new Son();
//console.log(son.getSon) //son
//console.log(son.getFather) //father
//console.log(son.getGrand) //grand

//console.log(father.getSon) //undefined
//console.log(father.getFather) //father
//console.log(father.getGrand) //grand

//console.log(grand.getSon) //undefined
//console.log(grand.getFather) //undefined
//console.log(grand.getGrand) //grand
上述代码,就是形成了一条原型链,
Son的原型是已经实例化的father对象,Father的原型是已经实例化的grand对象,
那么实例化的son就可以调用Grand构造器和Father构造器中的属性,
而相反地,实例化的father就无法调用Son构造器中的方法或属性,实例化的grand同理

说说你对作用域链的理解

1
2
3
4
5
1、作用域就是变量与函数的可访问范围

2、一般情况下,变量取值到创建这个变量的函数的作用域中取值。 但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链


typeof 与 instanceof 区别

1
2
3
4
5
6
7

typeofinstanceof都是判断数据类型的方法,区别如下:

typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

ajax、axios、jsonp的理解

1
2
3
4
5
6
7
8
9
10
11
1、jsonp是一种可以解决跨域问题的方式,就是通过动态创建script标签用src引入外部文件实现跨域,script加载实际上就是一个get请求,并不能实现post请求。(其他实现跨域的方法有:iframe,window.name,postMessage,CORS...)

2、ajax是一种技术,ajax技术包含了get和post请求的,但是它仅仅是一种获取数据的技术,不能直接实现跨域,只有后台服务器配置好Access-Control-Allow-Origin,才可以实现请求的跨域。

4、axios是通过promise实现对ajax技术的一种封装,axios是ajax,ajax不止axios。

总结:

juery的$.ajax实现get请求能跨域是因为jsonp或者因为原生ajax和服务器的配合,post请求能跨域就只能是因为原生ajax和服务器的配合


ajax的请求过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ajax 提交 post 请求的数据
// 1. 创建核心对象
var xhr = new XMLHttpRequest();
// 2. 准备建立连接
xhr.open("POST", "register.php", true);
// 3. 发送请求
// 如果要POST提交数据,则需要设置请求头
// 有的面试官会问为什么要设置请求头? 知道请求正文是以什么格式
// Content-Type: application/x-www-form-urlencoded,请求正文是类似 get 请求 url 的请求参数
// Content-Type: application/json,请求正文是一个 json 格式的字符串
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// 发送数据
xhr.send(querystring);
// 4. 处理响应
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { // 请求处理完毕,响应就绪
if (xhr.status === 200) { // 请求成功
var data = xhr.responseText;
console.log(data);
}
}
}


ajax请求的时候get 和post方式的区别

1
2
3
4
5
6
7
1、get请求不安全,post安全 ;

2、get请求数据有大小限制,post无限制 ;

3、get请求参数会在url中显示,容易被他人窃取,post在请求体中,不会被窃取;

4、post需要设置请求头。

什么是事件委托以及优缺点

1
2
3
4
5
6
7
8
9
10
11
js事件委托就是利用冒泡的原理,把本应该添加到某个元素上的事件委托给他的父级,从而减少DOM交互达到网页优化。

优点:

1.可以大量节省内存占用,减少事件注册。比如ul上代理所有li的click事件就很不错。 2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

缺点:

事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。


DOM操作与BOM操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(1) DOM操作
例如:document.getElementById 就是dom操作
DOM事件模型和事件流
DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
2)目标阶段:真正的目标节点正在处理事件的阶段;
3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
如何阻止冒泡?
通过 event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行。

事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理。
事件代理优点:
使代码简洁;减少浏览器的内存占用;

(2) BOM操作
BOM(浏览器对象模型)是浏览器本身的一些信息的设置和获取,例如获取浏览器的宽度、高度,设置让浏览器跳转到哪个地址。
例如:window.screen对象:包含有关用户屏幕的信息
window.location对象:用于获得当前页面的地址(URL),并把浏览器重定向到新的页面
window.history对象:浏览历史的前进后退等
window.navigator对象:常常用来获取浏览器信息、是否移动端访问等等

跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
什么是跨域?
当协议、子域名、主域名、端口号中任意一个不相同时都算做不同域,不同域之间相互请求资源,就算作“跨域”。

常见的几种跨域解决方案
JSONP:利用同源策略对 script 标签不受限制,不过只支持GET请求
为什么jsonp只支持get请求?
如果看过 JSONP 库的源码就知道,常见的实现代码其实就是 document.createElement(‘script’) 生成一个 script 标签,然后插 body 里而已。在这里根本没有设置请求格式的余地。

所以JSONP的实现原理就是创建一个script标签, 再把需要请求的api地址放到src里. 这个请求只能用GET方法, 不可能是POST

CORS:实现 CORS 通信的关键是后端,服务端设置 Access-Control-Allow-Origin 就可以开启,备受推崇的跨域解决方案,比 JSONP 简单许多

Node中间件代理或nginx反向代理:主要是通过同源策略对服务器不加限制

vue-cli代理跨域:devServer

前端vue解决跨域

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
VUE2.0中常用proxy来解决跨域问题
步骤1、在项目config目录下面有个index文件中设置如下代码片段
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: { // 配置跨域
'/api':{
target:`http://www.baidu.com`, //请求后台接口
changeOrigin:true, // 是否允许跨域
pathRewrite:{
'^/api' : '/api' // 重写请求,//重写路径 比如'/api/aaa/ccc'重写为'/aaa/ccc'

}
}
},
}
}
步骤2、创建axioss实例时,将baseUrl设置为 ‘/api’
const http = axios.create({
timeout: 1000 * 1000000,
withCredentials: true,
BASE_URL: '/api'
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})

Set 和 Map有什么区别

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
(1) Map是键值对,Set是值得合集,当然键和值可以是任何的值;
(2) Map可以通过get方法获取,而set不能因为它只有值;
(3) 都能通过迭代器进行forof遍历;
(4) Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储

MapSet 数据结构它们都是以构造函数的形式出现的,所以我们通常使用 new Set()或者 new Map()的形式初始化的

Map(字典)
初始化 map 对象
let myMap = new Map();

初始化 map 时传入数据(默认接收一个二维数组)
let defaultMap = new Map([['name', '张三'], ['age', 20]]); //打印 {'name' => '张三', 'age' => 20}

插入数据
myMap.set('name', '小猪课堂'); // 字符串作为键
myMap.set(12, '会飞的猪'); // number 类型作为键
myMap.set({}, '知乎'); // 对象类型作为键

获取长度
let myMapSize = myMap.size;

获取值
let objKey = {};
myMap.set('name', '小猪课堂'); // 字符串作为键
myMap.set(12, '会飞的猪'); // number 类型作为键
myMap.set(objKey, '知乎'); // 对象类型作为键

let name = myMap.get('name');
let age = myMap.get(12);
let any = myMap.get(objKey);

console.log(name, age, any); // 小猪课堂 会飞的猪 知乎
上段代码中需要注意的是不能使用 myMap.get({})的形式获取数据,因为 objKey!=={}。

删除某个值
myMap.delete('name');

判断某个值是否存在
myMap.has('name'); // 返回 bool 值

Set(集合)

初始化Set
let mySet = new Set();

初始化Set对象带有默认值:(和Map类似,Set初始化时也可以初始化默认数据。)
let defaultSet = new Set(['张三', 12, true]); //打印 {'张三', 12, true}

插入数据
mySet.add(1);
mySet.add('小猪课堂');

获取长度
let mySetSize = mySet.size;

获取值(由于Set对象存储的不是键值对形式,所以未提供get方法获取值,我们通常遍历它获取值)
mySet.forEach((item) => {
console.log(item)
})

删除某个值
mySet.delete(1);

判断某个值是否存在
mySet.has(1); // 返回Boolean值

//Map与对象的互换
const obj = {}
const map = new Map([
['a', 2],
['b', 3]
])
for (let [key, value] of map) {
obj[key] = value
}
console.log(obj)
// {a: 2, b: 3}

常见的检测数据类型的几种方式

1
2
3
4
5
6
7
8
9
10
11
(1) typeof 其中数组、对象、null都会被判断为Object,其他判断都正确
(2) instanceof 只能判断引用数据类型,不能判断基本数据类型
(3) constructor 它有2个作用 一是判断数据的类型,二是对象实例通过constructor对象访问它的构造函数。需要注意的事情是如果创建一个对象来改变它的原型,constructor就不能来判断数据类型了
(4) Object.prototype.toString.call() 使用 object 对象的原型方法 tostring 来判断数据类型

instanceoftypeof的区别:

instanceof 返回值为布尔值。用于判断一个变量是否属于某个对象的实例。

typeof 返回值是一个字符串, 用来说明变量的数据类型。
typeof 一般只能返回如下几个结果: number, boolean, string, function, object, undefined

怎么把类数组转换为数组

1
2
3
4
5
6
7
8
9
10
11
//通过call调用数组的slice方法来实现转换
Array.prototype.slice.call(arrayLike)

//通过call调用数组的splice方法来实现转换
Array.prototype.splice.call(arrayLike,0)

//通过apply调用数组的concat方法来实现转换
Array.prototype.concat.apply([],arrayLike)

//通过Array.from方法来实现转换
Array.from(arrayLike)

解构赋值

1
2
3
let a = 1; let b = 2;  如果在不声明第三个变量的前提下,使a=2, b=1

答案:[a, b] = [b, a]

如何利用es6快速的去重?

1
2
3
let arr = [23, 12, 13, 33, 22, 12, 21]

let item = [...new Set(arr)]

Promise 面试题 以下代码的执行结果是?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const promise = new Promise((resolve, reject) => {

console.log(1)

resolve()

console.log(2)

})

promise.then(() => {

console.log(3)

})

console.log(4)

答案:1,2,4,3

解释:以上考察的是关于promise的原理,promise的构造函数是-同步执行-的,当new Promise的一瞬间,1,2 就立刻被执行,而 .then方法是-异步执行-的,当执行完12之后,会执行输出4,最后执行输出3

1

1

1

1
2

...补充中
------ The End ------
您的认可是我不断进步的动力!