JS有哪些基本数据类型
- Boolean
- Null
- Undefined
- Number
- String
- Symbol(ES6新定义,Symbol生成一个全局唯一、表示独一无二的值)
- Object(Array,Function,Object)
对象是引用类型,在使用过程会遇到深拷贝和浅拷贝的问题1
2
3
4
5let a = {name: 'whh', age: 18};
let b = a;
b.name = 'lsd';
console.log(a); //{name:'lsd',age:18}
console.log(b); //{name:'lsd',age:18}
关于闭包
闭包的定义: 在一个函数里面嵌套另一个函数 被嵌套的那个函数的作用域是一个闭包。1
2
3
4
5
6function a() {
let a = 1;
function b() {
console.log(a)
}
}
闭包的用途:
- 读取函数内部的变量。
- 让这些变量的值始终保存在内存中。
使用闭包的注意点: - 由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题,在IE中可能导致内存泄漏。解决方法是,在退出函数之前,将不适用的局部变量全部删除。
- 闭包会在父函数外部改变父函数内部变量的值。不要随便更改父函数内部变量的值。
经典面试题:循环中使用闭包解决var定义函数的问题1
2
3
4
5for(var i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
首先因为setTimeout是个异步函数,所以会先把所有循环执行完毕,这时候i就是6了,所以会输出一堆6。有三种解决办法:
使用闭包
1
2
3
4
5
6
7for(var i=1;i<=5;i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000);
})(i);
}使用setTimeout的第三个参数
1
2
3
4
5for(var i=1;i<=5;i++) {
setTimeout(function timer(j) {
console.log(j);
},i*1000,i);
}使用let定义i
1
2
3
4
5for(let i=1;i<=5;i++) {
setTimeout(function timer() {
console.log(i);
},i*1000);
}
因为let会创建一个块级作用域,详细解释请阅读文章ES6手册
this的指向
在JS中,this是指正在执行函数的”所有者”,或者更确切地说,指将当前函数作为方法的对象。
- 当以方法的形式被调用时(如person.fullname()): this指向父级对象。
- 否则: 严格模式下为undefined 非严格模式下为window(最外层对象)。
- 当作为构造器被调用时,this指代即将生成的对象。
- 箭头函数中的this取决于它外面的第一个不是箭头函数的this。
call,apply,bind的区别
都是用于绑定this的指向 call和apply不同点在于apply接收一个数组传参(除了第一个传参) 视情况选择 立即执行
bind与call,apply不同 返回一个新函数 而不立即执行。
bind()方法会创建一个新函数。当这个新函数被调用时,bind()的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN)1
2
3
4
5
6
7
8
9
10var whh = {
name: '王花花',
}
var lsd = {
name: '李拴蛋',
}
yo.call(whh,'赵可爽',1,2,3); //直接执行
yo.apply(whh,['赵可爽',1,2,3]); //直接执行
var yo2 = yo.bind(whh); //将绑定了新环境的function返回等待被调用
yo2('赵可爽');
for和for…in的区别
遍历数组时的异同: for循环,数组下标的typeof类型为number,for…in循环数组下标的typeof类型为string。
1
2
3
4
5
6
7
8
9let arr = ['whh','lsd','aks','lbb'];
for(let i=0; i<arr.length; ++i) {
console.log(typeof i); //number
}
let arr = ['whh','lsd','aks','lbb'];
for(let it in arr) {
console.log(typeof it); //string
}遍历对象时的异同: for循环无法遍历对象,获取不到obj.length。for…in循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组成的数组。
深浅拷贝
深浅拷贝都是针对引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型复制,则会进行地质的拷贝,最终两个变量指向同一份数据。1
2
3
4
5
6let a = {
age: 18,
};
let b = a;
a.age = 19;
console.log(b.age);
如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。通常在开发中我们不希望出现这样的问题,我们可以使用 浅拷贝 来解决这个问题。
- 浅拷贝:
通过Object.assign
1
2
3
4
5
6let a = {
age: 18,
};
let b = Object.assign({},a);
b.age = 20;
console.log(a.age); //18通过展开运算符…
1
2
3
4
5
6let a = {
age: 18,
};
let b = {...a};
a.age = 20;
console.log(b.age); //18通过遍历对象属性
1
2
3
4
5
6
7
8
9function clone(obj) {
let target = {};
for(var i in obj) {
if(obj.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
- 深拷贝:
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么两者享有相同的引用。要解决这个问题,需要引入深拷贝。
这个问题通常通过JSON.parse(JSON.stringify(obj))
来解决1
2
3
4
5
6
7
8
9
10
11let obj = {
name: 'whh',
age: 'lsd',
address: {
country: 'China',
province: 'Guangdong',
}
}
let new_obj = JSON.parse(JSON.stringify(obj));
new_obj.address.province = 'Fujian';
console.log(obj.address.province); //'Guangdong'
此方法的局限性: 会忽略undefined / 不能序列化函数 / 不能解决循环引用的对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj);
如果有这样一个循环引用对象,我们就不能用上面的办法进行深拷贝。
在遇到函数或者undefined的时候,该对象也不能正常的序列化。1
2
3
4
5
6
7let a = {
age: undefined,
jobs: function() {},
name: 'link',
}
let b = JSON.parse(JSON.stringify(a));
console.log(b); // {name: 'link'}
在上述情况中,我们会发现,该方法会忽略掉函数和undefined。我们可以使用lodash的深拷贝函数。
如果所需拷贝的对象中含有内置类型并且不包含函数,可以使用MessageChannel。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function structuralClone() {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2 = ev => resolve(ev.data);
port1.postMessage(obj);
})
}
let obj = {
a: 1,
c: {
c: b,
}
}
//该方法是异步的
//可以处理undefined和循环引用的对象
const clone = structuralClone(obj);
console.log(clone);
typeof
typeof对于基本类型,除了null都可以显示正确的类型。1
2
3
4
5
6
7
8typeof '1'; //'string'
typeof 1; //'number'
typeof undefined; //'undefined'
typeof true; // 'boolean'
typeof Symbol(); //'symbol'
typeof b; //'undefined' b未声明所以显示'undefined'
但是对于null来说,虽然它是基本类型,但它会显示object,这是JS中存在已久的bug。
typeof null; // 'object'
typeof对于对象,除了函数都会显示object。1
2
3typeof []; //'object'
typeof {}; //'object'
typeof console.log() //'function'
所以如果我们想获得一个变量的正确类型,可以使用Object.prototype.toString.call(obj)
。这样我们就可以获得一个类似[object Type]的字符串。1
Object.prototype.toString.call({}); //[object Object]
四则运算符
只有当加法运算时,其中一个值为字符串类型,就会把另一个也转换为字符串类型。在其他运算中,只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换: 将值转换为原始值、转换为数字、转换为字符串。
对于加号要注意这个表达式: 'a'++'b' -> 'aNaN'
因为+'b'
为NaN。
如何检查一个数字是否为整数?
检查一个数字是整数还是小数,可以将它对1进行取模,看看是否有余数。1
2
3
4
5function isInt(num) {
return num % 1 === 0;
}
console.log(isInt(0.1)); //false;
console.log(isInt(8)); //true;
原型
每个函数都有prototype属性,除了Function.prototype.bind(),该属性指向原型。
每个对象都有__proto_属性,指向了创建该对象的构造函数。实际它指向的是该构造函数的[[prototype]],但是[[prototype]]是内部属性,我们并不能访问到,所以使用__proto_来访问。
对象可以通过__proto_来访问不属于该对象的属性,__proto将对象连接起来组成原型链。
JS事件机制
DOM事件流存在三个阶段: 事件捕获、处于目标、事件冒泡。
事件捕获: 当鼠标点击或者触发DOM事件时,浏览器会从根节点开始由外到内进行事件传播,也就是说,当我们点击了子元素,父元素通过事件捕获的方式注册了事件,那么就会先触发父元素绑定的事件。
事件冒泡: 与事件捕获相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
DOM标准事件流触发的先后顺序为: 先捕获再冒泡,即当触发DOM事件时,会先进行事件捕获,捕获到事件源后通过事件传播进行事件冒泡。
事件代理: 如果一个节点中的子节点是动态生成的,并且它的子节点需要注册事件,那么就应该将事件注册在父节点上。
事件代理的优点:
- 节省内存
- 不需要给子节点注销时间
Event loop 事件循环
JS是门非阻塞单线程语言,在执行过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中,如果遇到异步的代码,会被挂起并加入到Task队列中(有多种task)。一旦执行栈为空,Event loop就会从Task队列中拿出需要执行的代码并放到执行栈中执行。
不同的任务源会被分配到不同的Task队列中,任务源可以分为微任务(microtask)和宏任务(macrotask)。在ES6规范中,microtask称为jobs,macrotask称为task。1
2
3
4
5
6
7
8
9
10
11
12
13
14console.log('script start');
setTimeout(function() {
console.log('setTimeout');
},0);
new Promise((resolve) => {
console.log('Promise');
resolve();
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
//script start => Promise => script end => promise1 => promise2 => setTimeout
微任务包括process.nextTick、promise、Object.observe、MutationObserver。
宏任务包括script、setInterval、setTimeout、setImmediate、I/O、UI rendering。
一次正确的Event loop顺序
- 执行同步代码 这属于宏任务。
- 执行栈为空 查询是否有微任务需要进行。
- 执行所有微任务。
- 必要的话渲染UI。
- 然后开始下一轮Event loop,执行宏任务中的异步代码。
项目上线前的性能优化
- 图片预加载,css样式表放在顶部且link链式引入,javascript放在底部body结束标签前。
- 减少http请求,图片静态资源放在cdn托管。
- 减少DOM操作次数,优化javascript性能。
- 减少DOM元素数量,合理利用:after、:before等伪类。
- 使用dns-prefetch对项目中用到的域名进行DNS预解析,减少 DNS 查询,如:
<link rel=”dns-prefetch” href=”//github.com”/>
; - JS、CSS压缩 合并
- 图片压缩 合并(图片的合并可以使用CSS Spirite 方法就是把一些小图用ps合成一张图,用CSS定位显示每张图片的位置)
gulp图片压缩代码:1
2
3
4
5
6//压缩image
gulp.task('imagemin', function () {
gulp.src('./public/**/*.{png,jpg,gif,ico,jpeg}')
.pipe(imagemin())
.pipe(gulp.dest('./public'));
});