JavaScript基础知识面试题
JavaScript基础知识面试题
原始类型
提示
JavaScript 中原始类型有六种,原始类型既只保存原始值,是没有函数可以调用的。
六种原始类型
- string
- number
- boolean
- null
- undefined
- symbol
注意
为什么说原始类型没有函数可以调用,但'1'.toString()
却又可以在浏览器中正确执行?
因为'1'.toString()
中的字符串'1'
在这个时候会被封装成其对应的字符串对象,以上代码相当于new String('1').toString()
,因为new String('1')
创建的是一个对象,而这个对象里是存在toString()
方法的。
null到底是什么类型
现在很多书籍把null
解释成空对象,是一个对象类型。然而在早期JavaScript
的版本中使用的是32位系统,考虑性能问题,使用低位存储变量的类型信息,000
开头代表对象,而null
就代表全零,所以将它错误的判断成Object
,虽然后期内部判断代码已经改变,但null
类型为object
的判断却保留了下来,至于null
具体是什么类型,属于仁者见仁智者见智,你说它是一个bug
也好,说它是空对象,是对象类型也能理解的通。
对象类型
提示
在 JavaScript 中,除了原始类型,其他的都是对象类型,对象类型存储的是地址,而原始类型存储的是值。
var a = [];
var b = a;
a.push(1);
console.log(b); // 输出[1]
在以上代码中,创建了一个对象类型a
(数组),再把a
的地址赋值给了变量b
,最后改变a
的值,打印b
时,b
的值也同步发生了改变,因为它们在内存中使用的是同一个地址,改变其中任何一变量的值,都会影响到其他变量。
对象当做函数参数
function testPerson(person) {
person.age = 52;
person = {
name: '李四',
age: 18
}
return person;
}
var p1 = {
name: '张三',
age: 23
}
var p2 = testPerson(p1);
console.log(p1.age); // 输出52
console.log(p2.age); // 输出18
代码分析:
testPerson
函数中,person
传递的是对象p1
的指针副本- 在函数内部,改变
person
的属性,会同步反映到对象p1
上,p1
对象中的age
属性发生了改变,即值为52 testPerson
函数又返回了一个新的对象,这个对象此时和参数person
没有任何关系,因为它分配了一个新的内存地址- 以上分析可以用如下图表示
typeof和instanceof
typeof
提示
typeof
能准确判断除null
以外的原始类型的值,对于对象类型,除了函数会判断成function
,其他对象类型一律返回object
typeof 1 // number
typeof '1' // string
typeof true // boolean
typeof undefined // undefined
typeof Symbol() // symbol
typeof [] // object
typeof {} // object
typeof console.log// function
instanceof
提示
instanceof
通过原型链可以判断出对象的类型,但并不是百分百准确
function Person(name) {
this.name = name;
}
var p1 = new Person();
console.log(p1 instanceof Person) // true
var str = new String('abc');
console.log(str instanceof String)// true
类型转换
JavaScript
中,类型转换只有三种:
- 转换成数字
- 转换成布尔值
- 转换成字符串
经典类型面试题
console.log([]==![]); // true
代码分析:
- 左侧是一个对象(数组)
- 右侧是一个布尔值,对象
[]
转换成布尔值true
,因为除了null
所有对象都转换成布尔值,所以![]
结果为false
- 此时相当于
对象==布尔值
,依据类型转换规则,转换成数字类型进行比较 - 对象(空数组)转换成
0
,布尔值false
转换成0
- 即
0==0
,返回true
类型转换规则,如下图:
== 和 ===
如何你对上面的例子还一知半解,那么我们来详细介绍一下==
和===
的规则以及区别。
===严格相等
提示
===
叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1
的结果是false
,因为一边是string
,另一边是number
。
console.log('1'===1); // 输出false
==不严格相等
提示
==
不像===
那样严格,对于一般情况,只要值相等,就返回true
,但==
还涉及一些类型转换,它的转换规则如下:
- 两边的类型是否相同,相同的话就比较值的大小,例如
1==2
,返回false
- 类型不相同会进行类型转换
- 判断的是否是
null
和undefined
,是的话就返回true
- 判断的类型是否是
String
和Number
,是的话,把String
类型转换成Number
,再进行比较 - 判断其中一方是否是
Boolean
,是的话就把Boolean
转换成Number
,再进行比较 - 如果其中一方为
Object
,且另一方为String
、Number
或者Symbol
,会将Object
转换成原始类型后,再进行比较
1 == {id: 1, name: 'AAA'}
↓
1 == '[object Object]'
转boolean
除了undefined
、null
、false
、0
、-0
、NaN
和空字符串转换成false
以外,其他所有值都转换成true
,包括所有对象。
对象转原始类型
对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:
- 是否已经是原始类型,是则直接返回
- 调用
valueOf()
,如果转换为原始类型,则返回 - 调用
toString()
,如果转换为原始类型,则返回 - 也可以重写
Symbol.toPrimitive()
方法,优先级别最高 - 如果都没有返回原始类型,会报错
var obj = {
value: 0,
valueOf() {
return 1;
},
toString() {
return '2'
},
[Symbol.toPrimitive]() {
return 3
}
}
console.log(obj + 1); // 输出4
对象转原始类型应用
// 问:如何使if(a==1&&a==2&&a==3) {console.log('true')};正确打印'true'
var a = {
value: 0,
valueOf() {
this.value++;
return this.value;
}
}
if(a==1 && a==2 && a==3) {
console.log('true'); // 输出true
}
代码分析:
- 重写对象
a
的valueOf()
方法,使value
属性每次调用时自增 - 当判断
a==1
时,第一次调用valueOf()
方法,此时value
等于1,判断1==1
,继续向下走 - 判断
a==2
时,第二次调用valueOf()
方法,此时value
等于2,判断2==2
,继续向下走 - 判断
a==3
时,第三次调用valueOf()
方法,此时value
等于3,判断3==3
,if
判断结束 if
条件判断为true && true && true
,执行console.log('true')
,打印true
new构造调用的过程
无论是通过字面量还是通过new
进行构造函数调用创建出来的对象,其实都一样。调用new
的过程如下:
- 创建一个新对象
- 原型绑定
- 绑定this到这个新对象上
- 返回新对象
this全解析
JavaScript
中的this
只有如下几种情况,并按他们的优先级从低到高划分如下:
- 独立函数调用,例如
getUserInfo()
,此时this
指向全局对象window
- 对象调用,例如
stu.getStudentName()
,此时this
指向调用的对象stu
call()
、apply()
和bind()
改变上下文的方法,this
指向取决于这些方法的第一个参数,当第一个参数为null
时,this
指向全局对象window
- 箭头函数没有
this
,箭头函数里面的this
只取决于包裹箭头函数的第一个普通函数的this
new
构造函数调用,this
永远指向构造函数返回的实例上,优先级最高。
var name = 'global name';
var foo = function() {
console.log(this.name);
}
var Person = function(name) {
this.name = name;
}
Person.prototype.getName = function() {
console.log(this.name);
}
var obj = {
name: 'obj name',
foo: foo
}
var obj1 = {
name: 'obj1 name'
}
// 独立函数调用,输出:global name
foo();
// 对象调用,输出:obj name
obj.foo();
// apply(),输出:obj1 name
obj.foo.apply(obj1);
// new 构造函数调用,输出:p1 name
var p1 = new Person('p1 name');
p1.getName();
this解析流程图
闭包
提示
当一个函数能够记住并访问它所在的词法作用域的时候,就产生了闭包,即使函数是在词法作用域之外执行
闭包的几种表现形式
提示
- 返回一个函数
- 作为函数参数传递
- 回调函数
- 非典型闭包IIFE(立即执行函数表达式)
返回一个函数:这种形式的闭包在JavaScript
的代码编写中,是非常常见的一种方式。
var a = 1;
function foo(){
var a = 2;
// 这就是闭包
return function(){
console.log(a);
}
}
var bar = foo();
// 输出2,而不是1
bar();
作为函数参数传递:无论通过何种手段将内部函数传递到它所在词法作用域之外,它都会持有对原始作用域的引用,无论在何处执行这个函数,都会产生闭包。
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
回调函数:在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
// 定时器
setTimeout(function timeHandler(){
console.log('timer');
},100)
// 事件监听
$('#container').click(function(){
console.log('DOM Listener');
})
IIFE:IIFE(立即执行函数表达式)并不是一个典型的闭包,但它确实创建了一个闭包。
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
经典循环和闭包面试题
提示
以下代码运行结果是什么,如何改进?
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
代码分析:
for
循环创建了5个定时器,并且定时器是在循环结束后才开始执行for
循环结束后,用var i
定义的变量i
此时等于6- 依次执行五个定时器,都打印变量
i
,所以结果是打印5次6
第一种改进方法:利用IIFE(立即执行函数表达式)
当每次for
循环时,把此时的i
变量传递到定时器中
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, i*1000)
})(i)
}
第二种方法:setTimeout
函数的第三个参数,可以作为定时器执行时的变量进行使用
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, i*1000, i)
}
第三种方法(推荐):在循环中使用let i
代替var i
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
原型、原型链
撰写中......
浅拷贝、深拷贝
由于JavaScript
中对象是引用类型,保存的是地址,深、浅拷贝的区别是,当拷贝结束后,在一定程度上改变原对象中的某一个引用类型属性的值,新拷贝出来的对象依然受影响的话,就是浅拷贝,反之就是深拷贝。
浅拷贝的几种实现方法
提示
- 利用
Object.assign()
方法 - 利用
...
扩展运算符
第一种方法: Object.assign()
会拷贝原始对象中的所有属性到一个新对象上,如果属性为对象,则拷贝的是对象的地址,改变对象中的属性值,新拷贝出来的对象依然会受影响。
var obj = {
name: '张三',
age: 23,
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = Object.assign({}, obj);
obj.job.money = 21;
console.log(newObj.name); // 输出张三
console.log(newObj.age); // 输出23
console.log(newObj.job.money);// 输出21,受影响
第二种方法:...
扩展运算符是ES6
新增加的内容
var obj = {
name: '张三',
age: 23,
isStudent: false
}
var newObj = {...obj};
console.log(newObj.name); // 输出张三
console.log(newObj.age); // 输出23
深拷贝几种实现方式
提示
- 配合使用
JSON.parse()
和JSON.stringify()
两个函数(局限性比较大) - 实现自己的简易深拷贝方法
lodash
第三方库实现深拷贝
第一种方法: 利用JSON
的序列化和反序列化方法,可以实现简易对象深拷贝,但此种方法有较大的限制:
- 会忽略属性值为
undefined
的属性 - 会忽略属性为
Symbol
的属性 - 不会序列化函数
- 不能解决循环引用的问题,直接报错
var obj = {
name: '张三',
age: 23,
address: undefined,
sayHello: function() {
console.log('Hello');
},
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = JSON.parse(JSON.stringify(obj));
obj.job.money = 21;
console.log(newObj.name); // 输出张三
console.log(newObj.age); // 输出23
console.log(newObj.job.money); // 输出12
console.log(newObj.address); // 报错
console.log(newObj.sayHello());// 报错
第二种: 实现自己简易的深拷贝函数
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null;
}
if(!isObject(obj)) {
throw new Error('非对象');
}
var isArray = Array.isArray(obj);
var newObj = isArray ? [...obj] : {...obj};
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(newObj[key]) ? deepClone(newObj[key]) : newObj[key];
})
return newObj;
}
var obj = {
name: 'AAA',
age: 23,
job: {
name: 'FE',
money: 12000
}
}
var cloneObj = deepClone(obj);
obj.job.money = 13000;
console.log(obj.job.money); // 输出13000
console.log(cloneObj.job.money);// 输出12000
第三种方法: 使用lodash第三方函数库实现(需要先引入lodash.js)
var obj = {
name: '张三',
age: 23,
isStudent: false,
job: {
name: 'FE',
money: 12
}
}
var newObj = _.cloneDeep(obj);
obj.job.money = 21;
console.log(newObj.name); // 输出张三
console.log(newObj.age); // 输出23
console.log(newObj.job.money);// 输出12,不受影响
继承
在JavaScript
ES6之前,实现继承需要依赖原型、原型链和构造函数等等技术手段组合使用,在ES6之后,可以使用Class
类继承(并没有真正的类,只是一个语法糖,实质依然是函数)
继承的几种方式
- 原型链实现继承
- 借用构造函数实现继承
- 组合继承
- 寄生组合继承
- 类继承
原型链实现继承
提示
通过重写子类的原型,并将它指向父类的手段实现。这种方式实现的继承,创建出来的实例既是子类的实例,又是父类的实例。它有如下几种缺陷:
- 不能向父类构造函数传参
- 父类上的引用类型属性会被所有实例共享,其中一个实例改变时,会影响其他实例
function Animal() {
this.colors = ['red','blue'];
}
function Dog(name) {
this.name = name;
}
Dog.prototype = new Animal();
var dog1 = new Dog('旺财');
var dog2 = new Dog('钢镚');
dog2.colors.push('yellow');
console.log(dog1.colors); // ["red", "blue", "yellow"]
console.log(dog2.colors); // ["red", "blue", "yellow"]
console.log(dog1 instanceof Dog); // true
console.log(dog1 instanceof Animal);// true
借用构造函数实现继承
提示
借用构造函数实现继承,通过在子类中使用call()
方法,实现借用父类构造函数并向父类构造函数传参的目的。但这种方法,无法继承父类原型对象上的属性和方法。
function Animal(name) {
this.name = name;
this.colors = ['red','blue'];
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eating!');
}
function Dog(name) {
Animal.call(this,name);
}
var dog1 = new Dog('旺财');
var dog2 = new Dog('钢镚');
dog2.colors.push('yellow');
console.log(dog1.colors); // ["red", "blue"]
console.log(dog2.colors); // ["red", "blue", "yellow"]
console.log(dog1 instanceof Dog); // true
console.log(dog2 instanceof Animal);// false
console.log(dog1.eat()); // 报错
组合继承
提示
组合继承是组合了原型链继承和借用构造函数继承这两种方法,它保留了两种继承方式的优点,但它并不是百分百完美的:
- 父类构造函数被调用多次。
function Animal(name) {
this.name = name;
this.colors = ['red','blue'];
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eatting');
}
function Dog(name) {
Animal.call(this,name);
}
Dog.prototype = new Animal(); // 第一次调用
var dog1 = new Dog('dog1'); // 第二次调用
var dog2 = new Dog('dog2'); // 第三次调用
dog1.colors.push('yellow');
console.log(dog1.name); // 输出dog1
console.log(dog2.colors);// 输出['red','blue']
console.log(dog2.eat()); // 输出dog2 is eatting
寄生组合继承
提示
寄生组合继承是在组合继承的基础上,采用Object.create()
方法来改造实现
function Animal(name) {
this.name = name;
this.colors = ['red','blue'];
}
Animal.prototype.eat = function() {
console.log(this.name + ' is eatting');
}
function Dog(name) {
Animal.call(this,name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
var dog1 = new Dog('dog1');
var dog2 = new Dog('dog2');
dog1.colors.push('yellow');
console.log(dog1.name); // 输出dog1
console.log(dog2.colors);// 输出['red','blue']
console.log(dog2.eat()); // 输出dog2 is eatting
Class实现继承
提示
运用ES6 class新特性来实现继承
class Animal {
constructor(name) {
this.name = name;
this.colors = ['red','blue'];
}
eat() {
console.log(this.name + ' is eatting');
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
}
var dog1 = new Dog('dog1');
var dog2 = new Dog('dog2');
dog1.colors.push('yellow');
console.log(dog1.name); // 输出dog1
console.log(dog2.colors);// 输出['red','blue']
console.log(dog2.eat()); // 输出dog2 is eatting
ES6
本章节只介绍ES6常考知识点
var、let和const的区别
提示
var
声明的变量会提升到作用域的顶部,而let
和const
不会进行提升var
声明的全局变量会被挂载到全局window
对象上,而let
和const
不会var
可以重复声明同一个变量,而let
和const
不会var
声明的变量作用域范围是函数作用域,而let
和const
声明的变量作用域范围是块级作用域。const
声明的常量,一旦声明则不能再次赋值,再次赋值会报错(更改对象属性不会,因为对象地址没有变)
作用域提升:
console.log(a); // 输出undefined
console.log(b); // 报错
console.log(PI); // 报错
var a = 'abc';
let b = 'ABC';
const PI = 3.1415;
挂载到全局变量:
var a = 'abc';
let b = 'ABC';
const PI = 3.1415;
console.log(window.a); // 输出abc
console.log(window.b); // 输出undefined
console.log(window.PI); // 输出undefined
重复声明变量:
var a = 'abc';
var a;
console.log(a); // 输出abc
let b = 'ABC';
let b;// 报错
变量的作用域范围:
function foo() {
var flag = true;
if(flag) {
var a = 'abc';
let b = 'ABC';
console.log(a); // 输出abc
console.log(b); // 输出ABC
}
console.log(a); // 输出abc
console.log(b); // 报错
}
foo();
const常量:
const PI = 3.1415;
PI = 3.1415926; // 报错
扩展/收缩符
提示
ES6新增加的运算符...
,称为扩展或者收缩,具体作用取决于到底如何使用。
// ...的扩展
function foo(x,y,z) {
console.log(x,y,z); // 输出1,2,3
}
var arr = [1,2,3];
foo(...arr); // 扩展数组:ES6写法
foo.apply(null,arr); // 扩展数组:ES5写法
// ...的收缩
// 1.收集参数:ES6写法
function bar(...arr) {
console.log(arr); // 输出[1,2,3,4,5]
}
// 2.收集参数:ES5写法
function foo(){
var args = Array.prototype.slice.call(arguments);
console.log(args); // 输出[1,2,3,4,5]
}
bar(1,2,3,4,5);
foo(1,2,3,4,5)
解构赋值
提示
常用的解构赋值,有如下两种情况:
- 对象的解构
- 数组的解构
// 常用解构方式:解构对象 or 解构数组
// ES6之前的获取返回数组和返回对象的方式
function foo() {
return [1,2,3];
}
function bar() {
return {
X: 4,
Y: 5,
Z: 6
}
}
var arr = foo();
var a = arr[0];
var b = arr[1];
var c = arr[2];
var obj = bar();
var x = obj.X;
var y = obj.Y;
var z = obj.Z;
console.log(a,b,c); // 输出1,2,3
console.log(x,y,z); // 输出4,5,6
// ES6之后获取返回数组和返回对象的方式
var [A,B,C] = foo();
var {X,Y,Z} = bar();
console.log(A,B,c); // 输出1,2,3
console.log(X,Y,Z); // 输出4,5,6
字符串模板
提示
${内容}
:字符串模板里的内容可以是变量、函数调用以及表达式。
// 字符串模板
var name = 'why';
var age = 23;
var address = '广州';
// ES5拼接字符串
var str = '我叫:'+name+',我的年龄是:'+age+',我的地址是:'+address;
// ES6模板字符串
var newStr = `我叫:${name},我的年龄是:${age},我的地址是:${address}`;
console.log(str); // 输出:我叫:why,我的年龄是:23,我的地址是:广州
console.log(newStr); // 输出:我叫:why,我的年龄是:23,我的地址是:广州
map和set结构
Map结构: 对象是创建无序键值对数据结构映射的主要机制,在ES6之前,对象的属性只能是字符串,在ES6之后,Map
结构允许使用对象、数组等作为键。Map
结构的方法或者属性如下:
set()
:新增一个map结构的数据get(key)
:根据键获取值size
:获取map结构的长度delete(key)
:根据指定的键删除has(key)
:判断指定的键是否存在于map结构中keys()
遍历,values()
遍历,entries()
键值对遍历clear()
清空map结构
// Map结构
var map = new Map();
var x = { id: 1 },
y = { id: 2 };
// 设置map数据
map.set(x,'bar');
map.set(y,'foo');
// 获取map数据
console.log(map.get(x)); // 输出bar
console.log(map.get(y)); // 输出foo
// 获取map结构的长度
console.log(map.size); // 输出2
// 根据指定键删除map数据
map.delete(x);
// 根据指定的键判断是否存在于map结构中
console.log(map.has(x)); // 输出false
// 遍历map键
for(var key of map.keys()) {
console.log(key); // 输出{id:2}
}
// 遍历map值
for(var value of map.values()) {
console.log(value); // 输出foo
}
// 遍历map键值对
for(var item of map.entries()) {
console.log(item[0]); // 输出y
console.log(item[1]); // 输出{id:2}
}
Set结构: Set
是一个集合,它里面的值是唯一的,重复添加会被忽略(Set
结构不允许强制类型转换,1
和"1"
被认为是两个不同的值)。Set
结构的方法和属性如下:
add()
:添加新值size
:获取Set
结构的长度delete()
:根据指定的键删除has()
:判断指定的键是否存在Set
集合中keys()
遍历、values()
遍历、entries()
遍历clear()
:清空Set
结构
// Set结构
var set = new Set();
var x = { id: 1 };
var y = { id: 2 };
var a = 1;
var b = "1";
var c = true
// 添加Set数据
set.add(x);
set.add(y);
set.add(a);
set.add(b);
set.add(c);
// 获取Set数据的长度
console.log(set.size); // 输出5
// 删除Set数据
set.delete(c);
// 判断某个值是否存在Set结构中
console.log(set.has(c));// 输出false
// 遍历Set的键
for(var key of set.keys()) {
console.log(key); // 输出{id:1} {id:2} 1 "1"
}
// 遍历Set的值
for(var value of set.values()) {
console.log(value); // 输出{id:1} {id:2} 1 "1"
}
// 遍历Set的键值对
for(var item of set.entries()) {
console.log(item[0]); // 输出 {id:1} {id:2} 1 "1"
console.log(item[1]); // 输出 {id:1} {id:2} 1 "1"
}
Set结构的扩展运用: 数组去重、并集、交集、差集
// Set集合的运用:数组的去重、并集、交集、差集
var arr1 = [1,2,1,3,4,5];
var arr2 = [4,5,6,7];
// 去重:输出1,2,3,4,5
console.log(Array.from(new Set(arr1)));
// 并集:输出1,2,3,4,5,6,7
var union = Array.from(new Set([...arr1,...arr2]));
console.log(union);
// 交集:输出4,5
var intec = Array.from(new Set(arr1.filter(x => arr2.includes(x))));
console.log(intec);
// 差集
var diff1 = Array.from(new Set(arr1.filter(x => !arr2.includes(x))));
var diff2 = Array.from(new Set(arr2.filter(x => !arr1.includes(x))));
console.log(diff1); // 输出:1,2,3
console.log(diff2); // 输出:6,7
Proxy能干什么
在Vue2.0+
的版本中,Vue
使用Object.definedProperty()
方法来实现数据的响应式,在Vue3.0
的开发计划中,作者计划使用ES6新增加的Proxy
代理来实现数据的响应式,它相比于Object.definedProperty()
有如下几个特点:
Proxy
可以一次性为所有属性实现代理,无需遍历,性能更佳Proxy
能监听到以前使用Object.definedProperty()
监听不到的数据变动。- 由于是ES6新增加的特性,所以浏览器兼容性方面比
Object.definedProperty()差
let onWatch = function(obj,setBind, getLogger) {
return new Proxy(obj, {
get(target, property, receiver) {
getLogger(target, property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value, property);
return Reflect.set(target, property, value);
}
})
}
let obj = { a: 1}
let p = onWatch(obj, (value, property) => {
console.log(`监听到${property}属性的改变,其值为${value}`)
}, (target, property) => {
console.log(`监听到获取属性${property},其值为${target[property]}`)
})
p.a = 2; // 监听到a属性的改变,其值为2
console.log(a); // 监听到获取属性a,其值为2
数组的map、filter和reduce的区别
map: map
方法的作用是生成一个新数组(把原数组中的所有元素做一些变动,放进新数组中)
var newArr = [1,2,3].map(v => v*2);
console.log(newArr); // 输出[2,4,6];
filter: filter
方法的作用是从原数组中过滤出符合条件的元素,并生成一个新数组
var newArr = [1,2,3,4,5,6].filter(item => item%2==0);
console.log(newArr); // 输出[2,4,6];
reduce: reduce
方法的作用是通过回调函数的形式,把原数组中的元素最终转换成一个值,第一个参数是回调函数,第二个参数是初始值
var arr = [1,2,3,4,5,6];
var sum = arr.reduce((account, current) => {
return account + current;
}, 0);
console.log(sum); // 21
JavaScript异步
并发和并行
并行和并发是两个概念,容易混淆是因为并行和并发在中文意思上相近,其实在英文中,这是完全不相同的东西,并行(parallelism)、并发(concurrency)
概念理解
并行(parallelism):是微观概念,假设CPU有两个核心,则我们就可以同时完成任务A和任务B,同时完成多个任务的情况就可以称之为并行。
并发(concurrency):是宏观概念,现在有任务A和任务B,在一段时间内,通过任务之间的切换完成这两个任务,这种情况称之为并发。
回调函数
回调函数广泛存在于我们所编写的JavaScript
代码中,它表现在事件绑定,Ajax请求或者其他的情况下,一个回调函数可表现成如下形式
ajax(url, () => {
console.log('这里是回调函数');
})
回调地狱: 回调函数很好的解决了某些异步情况,但过度滥用回调函数会造成回调地狱,即回调函数过长,嵌套过深。过长或者嵌套过深的回调函数,会让回调函数存在强耦合关系,一旦有一个函数有所改动,那么可能会牵一发而动全身。一个回调地狱可能如下所示:
ajax(firstUrl, () => {
console.log('这里是首次回调函数');
ajax(secondUrl, () => {
console.log('这里是第二次回调函数');
ajax(threeUrl, () => {
console.log('这里是第三次回调函数');
// todo更多
})
})
})
Generator
在ES6之前,一个函数一旦执行将不会被中断,一直到函数执行完毕,在ES6之后,由于Generator
的存在,函数可以暂停自身,待到合适的机会再次执行。用Generator
可以解决回调地狱。
function *fetch() {
yield ajax(url, () => {console.log('这里是首次回调函数');});
yield ajax(url, () => {console.log('这里是第二次回调函数');});
yield ajax(url, () => {console.log('这里是第三次回调函数');});
}
var it = fetch();
var result1 = it.next();
var result2 = it.next();
var result3 = it.next();
Promise
Promise
翻译过来就是承诺的意思,Promise
一共有三种状态:pending(等待中)
、resolve(完成)
和reject(拒绝)
,这个承诺意味着在将来一定会有一个表决,并且只能表决一次,表决的状态一定是resolve(完成)
或者reject(拒绝)
,一个Promise
可能会是如下的形式:
// 普通的Promise
function foo() {
return new Promise((resolve,reject) => {
// 第一次表决有效,其后无论是resolve()还是reject()都无效
resolve(true);
resolve(false);
})
}
// Promise解决回调地狱
ajax(url).then(res => {
console.log('这里是首次回调函数');
}).then(res => {
console.log('这里是第二次回调函数');
}).then(res => {
console.log('这里是第三次回调函数');
})
Promise.all(): Promise.all()
方法是把一个或者几个Promise
组合在一个数组里,只有当数组中的所有Promise
全部表决完成,才返回。
var p1 = Promise.resolve(1);
var p2 = new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, 100);
})
var p3 = 3;
Promise.all([p1,p2,p3]).then((res) => {
console.log(res); // 输出[1,2,3]
})
Promise.race(): Promise.race()
方法把一个或者几个Promise
组合在一个数组里,只要数组中有一个表决了,就返回。
var p1 = Promise.resolve(1);
var p2 = new Promise((resolve) => {
setTimeout(() => {
resolve(2);
}, 100);
})
var p3 = 3;
Promise.race([p2,p1,p3]).then((res) => {
console.log(res); // 输出1
})
async/await
如果一个方法前面加上了async
,那么这个方法就会返回一个Promise
,async
就是将函数用Promise.resolve()
包裹了下,并且await
只能配合async
使用,不能单独出现。一个async/await
可能会是如下的形式:
// 普通的async/await
async function foo() {
let number = await 3; // await自动用promise.resolve()包装
console.log(number);
}
foo();
// async/await解决回调地狱
async function fetch() {
var result1 = await ajax(url1);
var result2 = await ajax(url2);
var result3 = await ajax(url3);
}
fetch();
setInterval、setTimeout和requestAnimationFrame
setTimeout setTimeout
延时执行某一段代码,但setTimeout
由于EventLoop
的存在,并不百分百是准时的,一个setTimeout
可能会表示如下的形式:
// 延时1s之后,打印hello,world
setTimeout(() => {
console.log('hello,world');
}, 1000)
setInterval: setInterval
在指定的时间内,重复执行一段代码,与setTimeout
类似,它也不是准时的,并且有时候及其不推荐使用setInterval
定时器,因为它与某些耗时的代码配合使用的话,会存在执行积累的问题,它会等耗时操作结束后,一起一个或者多个执行定时器,存在性能问题。一个setInterval
可能会表示如下的形式:
setInterval(() => {
console.log('hello,world');
}, 1000)
requestAnimationFrame: 翻译过来就是请求动画帧,它是html5专门用来设计请求动画的API,它与setTimeout
相比有如下优势:
- 根据不同屏幕的刷新频率,自动调整执行回调函数的时机。
- 当窗口处于未激活状态时,
requestAnimationFrame
会停止执行,而setTimeout
不会 - 自带函数节流功能
var progress = 0;
var timer = null;
function render() {
progress += 1;
if (progress <= 100) {
console.log(progress);
timer = window.requestAnimationFrame(render);
} else {
cancelAnimationFrame(timer);
}
}
//第一帧渲染
window.requestAnimationFrame(render);
EventLoop事件循环
进程和线程
提示
JavaScript
是单线程执行的,在JavaScript
运行期间,有可能会阻塞UI渲染,这在一方面说明JavaScript
引擎线程和UI渲染线程是互斥的。JavaScript
被设计成单线程的原因在于,JavaScript
可以修改DOM,如果在JavaScript
工作期间,UI还在渲染的话,则可能不会正确渲染DOM。单线程也有一些好处,如下:
- 节省内存空间
- 节省上下文切换时间
- 没有锁的问题存在
进程: CPU在运行指令及加载和保存上下文所需的时间,放在应用上一个程序就是一个进程,一个浏览器tab选项卡就是一个进程
线程: 线程是进程中更小的单位,描述了执行一段指令所需的时间。
执行栈
提示
可以把执行栈看成是一个存储函数调用的栈结构,遵循先进后出的原则,一个执行栈可能表现如下:
EventLoop
上面讲到函数会在执行栈中执行,那么当遇到异步代码后,该如何处理呢?其实当遇到异步代码的时候,会被挂起在Task队列中,一旦执行栈为空,就会从Task中拿出需要执行的代码执行,所以本质上讲JS中的异步还是同步行为。
如上图,可以看到,不同的异步任务是有区别的,异步任务又可以划分如下:
- 宏任务(
script
、setTimeout
、setInterval
、setImmidiate
、I/O
、UI Rendering
)可以有多个队列 - 微任务(
procress.nextTick
、Promise.then
、Object.observe
、mutataionObserver
)只能有一个队列
执行顺序: 当执行栈执行完毕后,会首先执行微任务队列,当微任务队列执行完毕再从宏任务中读取并执行,当再次遇到微任务时,放入微任务队列。
setTimeout(() => {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
})
}, 0)
setTimeout(() => {
console.log(3);
}, 0)
Promise.resolve().then(() => {
console.log(4);
})
console.log(5);
// 输出结果:5 4 1 2 3
代码分析:
console.log(5)
是唯一的同步任务,首先执行,输出5- 将所有异步任务放在Task队列中,挂起
- 同步任务执行完毕,开始执行微任务队列,即
Promise.then
,输出4 - 微任务队列执行完毕,执行宏任务队列
setTimeout
- 宏任务队列中首先执行同步任务,再次遇到微任务,放入微任务队列中,输出1
- 同步任务执行完毕,执行微任务队列,输出2
- 微任务队列执行完毕,执行宏任务队列
setTimeout
,输出3