这一篇面试内容是JavaScript的精髓所在,即使不是为了面试,平时也需要好好琢磨各个知识点中的原理,只要啃下了这一篇所有内容,JavaScript能力必定会上升一个新的阶段。
1.JavaScript类之间的继承
- (1)借助构造函数实现不完全继承,无法继承方法:
function Parent1() {
this.name = 'parent1';
}
Parent1.prototype.say = function() {
console.log('parent say hi')
}
function Child1() {
Parent1.call(this);
this.type = 'child1';
}
let child = new Child1()
let parent = new Parent1()
console.log(child); //Child1 {name: 'parent1', type: 'child1'}
console.log(child.say()); //报错
console.log(parent); //Parent1 {name: 'parent1'}
console.log(parent.say()); //parent say hi
- (2)借助原型链实现继承,所有的属性和方法都得去原型链上去找,因而找到的属性方法都是同一个,所以直接利用原型链继承是不现实的。
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3];
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();
console.log(new Child2()); //Child2 {type: 'child2'}
var s1 = new Child2();
var s2 = new Child2();
console.log(s1.play, s2.play); //[1,2,3] [1,2,3]
s1.play.push(4);
console.log(s1.play, s2.play); //[1,2,3,4] [1,2,3,4]
- (3)组合方式实现继承
function Parent3() {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
console.log(new Child3()); //Child3 {name: 'parent3', play: Array(3), type: 'child3'}
var s3 = new Child3();
var s4 = new Child3();
console.log(s3 instanceof Child3, s3 instanceof Parent3); //true true
s3.play.push(4);
console.log(s3.play, s4.play); //[1,2,3,4] [1,2,3]
- (4)组合继承的优化1
function Parent4() {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
var s5 = new Child4();
var s6 = new Child4();
s5.play.push(4);
console.log(s5.play, s6.play); //[1,2,3,4] [1,2,3]
console.log(s5 instanceof Child4, s5 instanceof Parent4); //true true
console.log(s5.constructor); //f Parent4() {}
- (5)组合继承的优化2(俗称寄生式继承)
function Parent5() {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
var s7 = new Child5();
var s8 = new Child5();
s7.play.push(4);
console.log(s7.play, s8.play); //[1,2,3,4] [1,2,3]
console.log(s7 instanceof Child5, s7 instanceof Parent5); //true true
console.log(s7.constructor); //f Child5() {}
2.什么是原型和原型链?有什么特点
- (1)原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。
- (2)原型链:由相互关联的原型组成的链状结构就是原型链。
- (3)特点:JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的话,就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象。
- 更多:JavaScript中的原型和原型链
- 更多:三张图搞懂JavaScript的原型对象与原型链
- 更多:JavaScript深入之从原型到原型链
3.说一下JavaScript的运行机制
认认真真看这篇文章即可,里面包含了大量且有用的知识点:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
4.JavaScript中的闭包
- (1)有权访问另一个函数作用域内变量的函数都是闭包。
- (2)闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。这是优点也是缺点,不必要的闭包只会徒增内存消耗!另外使用闭包也要注意变量的值是否符合你的要求,因为他就像一个静态私有变量一样。
- 更多:深入理解闭包系列第一篇——到底什么才是闭包
- 更多:JavaScript深入之闭包
5.描述new一个对象的过程
- (1)创建空对象:var obj = {};
- (2)设置新对象的constructor属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的prototype对象:
obj.__proto__ = context.prototype; - (3)使用新对象调用函数,函数中的this被指向新实例对象:
context.apply(obj, [...arguments].slice(1)); - (4)将初始化完毕的新对象地址,保存到等号左边的变量中
function Otaku (name, age) {
this.name = name;
this.age = age;
this.habit = 'Games';
}
Otaku.prototype.strength = 60;
Otaku.prototype.sayYourName = function () {
console.log('I am ' + this.name);
}
var person = new Otaku('Kevin', '18');
console.log(person.name) // Kevin
console.log(person.age) // 18
console.log(person.habit) // Games
console.log(person.strength) // 60
person.sayYourName(); // I am Kevin
// new实现
function objectFactory(context) {
const obj = {};
obj.__proto__ = context.prototype;
console.log([...arguments]) //[f,'test','22']
console.log([...arguments].slice(1)) //['test','22']
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
};
// 测试
var person1 = objectFactory(Otaku, 'test', '22')
console.log(person1.name) // test
console.log(person1.age) // 22
console.log(person1.habit) // Games
console.log(person1.strength) // 60
person1.sayYourName(); // I am test
【注意】
若构造函数中返回this或返回值是基本类型(number、string、boolean、null、undefined)的值,则返回新实例对象;若返回值是引用类型的值,则实际返回值为这个引用类型。
6.怎么理解js是单线程的
主要说一下异步以及事件循环机制,还有事件队列中的宏任务、微任务。
- macrotask: 主代码块,setTimeout,setInterval、setImmediate等。
- microtask: process.nextTick(相当于node.js版的setTimeout),Promise 。process.nextTick的优先级高于Promise。更具体的请看这里 这一次,彻底弄懂 JavaScript 执行机制
7.call、apply和bind的作用是什么?以及区别在哪
先看下简单的总结
- apply、call、bind 三者都是用来改变函数的this对象的指向的;
- apply、call、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
- apply、call、bind 三者都可以利用后续参数传参;
- bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
十分详细且到位的总结请看 JS中的call、apply、bind方法详解
8.对象(引用类型)如何转原始类型?
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:
- 如果已经是原始类型了,那就不需要转换了
- 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
- 调用 x.toString(),如果转换为基础类型,就返回转换的值
- 如果都没有返回原始类型,就会报错
当然你也可以重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
9.什么是浅拷贝?如何实现浅拷贝?
对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
另外我们还可以通过展开运算符 ... 来实现浅拷贝
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
10.什么是深拷贝?如何实现深拷贝?
深拷贝就是增加一个指针,并且申请一个新的内存地址,使这个增加的指针指向这个新的内存,然后将原变量对应内存地址里的值逐个复制过去
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该方法也是有局限性的:
- 会忽略 undefined
- 会忽略 symbol
- 会忽略函数
- 不能序列化函数
- 不能解决循环引用的对象
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel()
port2.onmessage = ev => resolve(ev.data)
port1.postMessage(obj)
})
}
var obj = {
a: 1,
b: {
c: 2
}
}
obj.b.d = obj.b
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
const clone = await structuralClone(obj)
console.log(clone)
}
test()
当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版
var deepCopy = function (obj) {
if (typeof obj !== 'object') return
var newObj = obj instanceof Array ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
}
}
return newObj
}
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3,
},
}
let newObj = deepCopy(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2
网上有些文章的深浅拷贝与以上内容有差入,可以前往看看,增加对其的理解程度
11.call和apply的区别是什么,哪个性能更好一些
- 区别: apply、call两者都是用来改变函数的this对象的指向的,它们的区别只是传递参数的形式不同
fn.call(obj,10,20,30)
fn.apply(obj,[10,20,30])
- 性能: call的性能要比apply好一些,尤其是传递给函数的参数超过三个的时候。所以后期开发的时候,可以使用call多一点。
- 应用场景:
let arr = [10,20,30]
let obj = {}
function fn(x,y,z){
console.log(x)
console.log(y)
console.log(z)
}
fn.apply(obj,arr) //x=10 y=20 z=30
fn.call(obj,arr) //x=[10,20,30] y=z=undefined
fn.call(obj,...arr) //x=10 y=20 z=30
- 性能测试: 任何代码的性能测试都是和测试环境有关系的,例如CPU、内存、GPU等电脑当前性能。不同浏览器也会导致性能上的不同。
let arr = [10,20,30]
let obj = {}
function fn(x,y,z){}
// console.time可以册数一段程序执行的时间
console.time('A')
for(let i=0;i<10000000;i++){
fn.apply(obj,arr)
}
console.timeEnd('A')
console.time('B')
for(let i=0;i<10000000;i++){
fn.call(obj,arr)
}
console.timeEnd('B')
12.什么是函数柯里化?以及应用场景和缺点是什么?
概念:
把接受多个参数的函数变成接受单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
使用场景:
- 参数复用
- 提前确认,避免每次都重复判断
- 延迟计算/运行:意思就是参数没有全部传递完的话程序是不会执行的
案例引入:
// 普通函数
function say(company,academy,name) {
console.log(`我的公司是${company},专业是${academy},名字是${name}`)
}
say('蜀国','武将','张飞')
say('蜀国','武将','关羽')
say('蜀国','武将','赵云')
// 函数柯里化
function say1(company) {
return function(academy) {
return function(name) {
console.log(`我的公司是${company},专业是${academy},名字是${name}`)
}
}
}
say1('蜀国')
/*
ƒ (academy) {
return function(name) {
console.log(`我的公司是${company},专业是${academy},名字是${name}`)
}
}
*/
say1('蜀国')('武将')
/*
ƒ (name) {
console.log(`我的公司是${company},专业是${academy},名字是${name}`)
}
*/
say1('蜀国')('武将')('张飞') //我的公司是蜀国,专业是武将,名字是张飞
let setCompany = say1('蜀国')
let setAcademy = setCompany('武将')
setAcademy('关羽') //我的公司是蜀国,专业是武将,名字是关羽
let setInfo = say1('蜀国')('武将')
setInfo('张三') //我的公司是蜀国,专业是武将,名字是张三
setInfo('关四') //我的公司是蜀国,专业是武将,名字是关四
setInfo('赵五') //我的公司是蜀国,专业是武将,名字是赵五
封装一个公用的函数柯里化的方法:
function say(company) {
return function(academy) {
return function(name) {
console.log(`我的公司是${company},专业是${academy},名字是${name}`)
}
}
}
// 参数fn:被柯里化的函数
function curry(fn) {
// 记录fn的参数个数
let len = fn.length;
return function temp(){
// 收集本地传递的参数
let args = [...arguments]
if(args.length >= len) {
return fn(...args)
} else {
return function() {
return temp(...args, ...arguments)
}
}
}
}
let r = curry(say)
r('a')('b')('c') //我的公司是a,专业是b,名字是c
let r1 = r('蜀国')('武将')
r1('赵云') //我的公司是蜀国,专业是武将,名字是赵云
r1('马超') //我的公司是蜀国,专业是武将,名字是马超
柯里化的其他应用:
1.提前确认,避免每次都重复判断
var addEvent = function(el, type, fn, capture) {
if(window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e)
}, capture)
} else if(window.attachEvent) {
el.attachEvent('on' + type, function(e){
fn.call(el, e)
})
}
}
上述代码,每次使用addEvent为元素添加事件的时候,都会走一遍if...else if...,其实只要一次判断就可以了,使用函数柯里化进行优化:
var addEvent = (function() {
if(window.addEventListener) { //高版本浏览器
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e)
}, (capture))
}
} else if(window.attachEvent) { //低版本浏览器
return function(el, sType, fn, capture) {
el.attachEvent('on' + sType, function(e) {
fn.call(el, e)
})
}
}
})()
初始addEvent的执行其实是实现了部分的应用(只有一次的if...else if...判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。
2.延迟计算/运行
let currySay = currying(say)
let fn1 = currySay('蜀国') //不执行say函数
let fn2 = fn1('武将') //不执行say函数
fn2('赵云') //执行say函数,输出结果
性能问题:
使用柯里化让程序具有了更多的自由度,但柯里化用到了arguments对象、递归、闭包等,频繁使用会给性能带来影响,只有在情况变得复杂时,才是柯里化大显身手的时候。
13.什么是发布订阅模式和观察者模式?以及它们之间的区别和应用场景是什么?
1.发布订阅模式: 我们假定存在一个信号中心,某个任务完成就向信号中心发布一个信号,其他任务可以向这个信号订阅这个模式,从而知道自己什么时候可以开始执行,这样的模式叫做发布订阅模式。代码实现:
let fs = require('fs')
// 发布 —> 中间代理 <— 订阅
function Events(){
this.calllbacks = []
this.results = []
}
// 订阅
Events.prototype.on = function(calllback){
this.calllbacks.push(calllback)
}
// 发布
Events.prototype.emit = function(data){
this.results.push(data)
this.calllbacks.forEach(c => c(this.results))
}
let e = new Events()
e.on(function(arr){
if(arr.length===3){
console.log(arr)
}
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/demo.txt','utf8',function(err, data){
e.emit(data)
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/name.txt','utf8',function(err, data){
e.emit(data||'默认值')
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/demo.txt','utf8',function(err, data){
e.emit(data)
})
2.观察者模式: 观察者模式没有信号中心,只有发布者和订阅者,并且发布者要知道订阅者的存在。观察者和被观察者,如果被观察者数据变化了,则通知观察者。
// 被观察者
class Subject {
constructor(name){
this.observers = [] //被观察者要存放到观察者中
this.name = name
this.state = '心情很嗨'
}
// 需要提供一个接受观察者的方法
attach(observer){
this.observers.push(observer) //存放所有的观察者
}
//更改被观察者的状态
setState(newState){
this.state = newState
this.observers.forEach(o => o.update(newState))
}
}
// 观察者
class Observer{
constructor(name){
this.name = name
}
update(newState){ //用来通知所有的观察者状态更新了
console.log(this.name +'说:'+ newState)
}
}
let sub = new Subject('小萌萌')
let o1 = new Observer('小家长')
let o2 = new Observer('大家长')
sub.attach(o1)
sub.attach(o2)
sub.setState('心情不好了')
3.它们之间的区别:
- 观察者模式是由具体目标调度,比如当事件触发的时候,Dep 就回去调用观察者的update方法,所以观察者的订阅者和发布者是存在依赖关系的
- 发布/订阅模式是由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在
- 观察者模式是高耦合,,目标和观察者是直接联系起来的,基于对象
- 发布订阅模式中,双方不知道对方的存在,而观察者模式中,基于自定义事件
- 观察者模式与发布订阅模式都是定义了一个一对多的依赖关系,当有关状态发生变更时则执行相应的更新
- 不同的是,在观察者模式中依赖于 Subject 对象的一系列 Observer 对象在被通知之后只能执行同一个特定的更新方法,而在发布订阅模式中则可以基于不同的主题去执行不同的自定义事件
- 相对而言,发布订阅模式比观察者模式要更加灵活多变。观察者模式和发布订阅模式本质上的思想是一样的,而发布订阅模式可以被看作是观察者模式的一个进阶版
- 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过调度中心进行通信
- 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反
- 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)
- 由以上的描述可以看出,发布订阅模式是松散耦合的,而观察者模式强耦合
4.应用场景:
- 观察者模式的场景:Vue的依赖追踪,原生事件
- 发布订阅模式的场景: React的合成事件,vue组件间通信的EventBus
14.什么是高阶函数?应用场景和常见的高阶函数有哪些?
概念:
英文叫Higher-order function。JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,其实指代的是参数是一个函数,函数返回函数(预置参数),这种函数就称之为高阶函数。
应用场景:
- 1.包装成一个高阶函数,批量生成函数
// 判断类型
// 1.普通Object.prototype.toString.call()判断方法
let a = 'hello'
console.log(Object.prototype.toString.call(a))
let b = {}
function isType(param, type) {
return Object.prototype.toString.call(param).includes(type)
}
console.log(isType(b,'Object'))
console.log(isType(b,'String'))
// 2.包装成一个高阶函数,批量生成函数
let types = ['String','Object','Array','Null','Undefined','Boolean']
let fns = {}
types.forEach(type=>{ //批量生成方法
fns['is'+type] = isType2(type)
})
function isType2(type) {
return function(obj) {
return Object.prototype.toString.call(obj).includes(type)
}
}
console.log(fns)
let c = true
console.log(fns.isBoolean(c)) //函数柯里化或者偏函数
- 2.解决异步问题
// after在多次之后执行
function after(times, callback){
let total = 0
return function(a){
total += a
if(--times == 0) {
callback(total)
}
}
}
let fn = after(3, function(total){ //当达到预计的次数,执行某个函数
console.log('after', total)
})
// 解决异步问题,fn只执行2次无法调用callback()
fn(1)
fn(2)
fn(3)
- 3.扩展原有的方法 || 重写原有的方法
// AOP:面向切片编程,把原来切成片,在中间加上自己的代码
// 装饰器:扩展原有的方法 || 重写原有的方法
function say(who) {
console.log(who+'说话ing')
}
Function.prototype.before = function(fn){
// this = say
let that = this
return function(){
fn()
that(...arguments) //...es6展开运算符,把arguments参数展开依次传入
}
}
let newFn = say.before(function(){
console.log('hi~')
})
newFn('小明')
- 4.node异步调用文件
// 案例一:node版本要求7.6以上
let fs = require('fs')
let arr = []
let i=0
function fn(data, index){
arr[index] = data
if(++i == 2){
console.log(arr)
}
}
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/demo.txt','utf8',function(err, data){
fn(data,0)
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/name.txt','utf8',function(err, data){
fn(data||'默认值',1)
})
// 案例二:node版本要求7.6以上
let fs = require('fs')
function after(times, callback){
let arr = []
return function(err,data){
if(err) {
throw new Error(err)
}
arr.push(data)
if(--times == 0) {
callback(arr)
}
}
}
let newfn = after(3, function(arr){
console.log(arr) //当异步完成后触发该方法
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/demo.txt','utf8',function(err, data){
newfn(err,data)
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/name.txt','utf8',function(err, data){
newfn(err, data||'默认值')
})
fs.readFile('D:/feng/WebInterview/B站前端大杂烩/demo.txt','utf8',function(err, data){
newfn(err,data)
})
内置的高阶函数:
- 1.map()
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
//map()传入的参数是pow,即函数对象本身。
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
//不需要map(),写一个循环,也可以计算出结果:
var f = function (x) {
return x * x;
};
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
result.push(f(arr[i]));
}
//的确可以,但是,从上面的循环代码,我们无法一眼看明白“把f(x)作用在Array的每一个元素并把结果生成一个新的Array”。
//所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,
//还可以计算任意复杂的函数,比如,把Array的所有数字转为字符串
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
- 2.reduce():再看reduce的用法。Array的reduce()把一个函数作用在这个Array的[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
//比方说对一个Array求和,就可以用reduce实现:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
- 3.filter():filter也是一个常用的操作,它用于把Array的某些元素过滤掉,然后返回剩下的元素。和map()类似,Array的filter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。
//例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
//把一个Array中的空字符串删掉,可以这么写:
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
});
arr; // ['A', 'B', 'C']
//回调函数:filter()接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示Array的某个元素。
//回调函数还可以接收另外两个参数,表示元素的位置和数组本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});
//利用filter,可以巧妙地去除Array的重复元素:
'use strict';
var r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index;
});
//去除重复元素依靠的是indexOf总是返回第一个元素的位置,后续的重复元素位置与indexOf返回的位置不相等,因此被filter滤掉了。
alert(r.toString());
- 4.sort(): 因为Array的sort()方法默认把所有元素先转换为String再排序,结果'10'排在了'2'的前面,因为字符'1'比字符'2'的ASCII码小。如果不知道sort()方法的默认排序规则,直接对数字排序,绝对栽进坑里!幸运的是,sort()方法也是一个高阶函数,它还可以接收一个比较函数来实现自定义的排序。
//要按数字大小排序,我们可以这么写:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]
//如果要倒序排序,我们可以把大的数放前面:
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]
//默认情况下,对字符串排序,是按照ASCII的大小比较的,现在,排序应该忽略大小写,按照字母序排序。
//要实现这个算法,不必对现有代码大加改动,只要我们能定义出忽略大小写的比较算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']
//忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
//sort()方法会直接对Array进行修改,它返回的结果仍是当前Array:
var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一对象
15.原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来讲下 class,其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。
class Person {}
Person instanceof Function // true
下面来看看原型实现继承,最常用的继承方式是原型继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。当然了,之前也说了在 JS 中并不存在类,class 的本质就是函数。
16.Proxy 可以实现什么功能?
如果你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
let p = new Proxy(target, handler)
target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。接下来我们通过 Proxy 来实现一个数据响应式
let onWatch = (obj, setBind, getLogger) => {
let handler = {
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)
}
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
在上述代码中,我们通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。
17.map, filter, reduce 各自有什么作用?
map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
['1','2','3'].map(parseInt)
- 第一轮遍历 parseInt('1', 0) -> 1
- 第二轮遍历 parseInt('2', 1) -> NaN
- 第三轮遍历 parseInt('3', 2) -> NaN
filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素,和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
let array = [1, 2, 4, 6]
let newArray = array.filter(item => item !== 6)
console.log(newArray) // [1, 2, 4]
reduce 可以将数组中的元素通过回调函数最终转换为一个值。如果我们想实现一个功能将函数里的元素全部相加得到一个值,可能会这样写代码
const arr = [1, 2, 3]
let total = 0
for (let i = 0; i < arr.length; i++) {
total += arr[i]
}
console.log(total) //6
但是如果我们使用 reduce 的话就可以将遍历部分的代码优化为一行代码
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
- 对于 reduce 来说,它接受两个参数,分别是回调函数和初始值,接下来我们来分解上述代码中 reduce 的过程
- 首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
- 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
- 在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
- 所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
当然 reduce 还可以实现很多功能,接下来我们就通过 reduce 来实现 map 函数
const arr = [1, 2, 3]
const mapArray = arr.map(value => value * 2)
const reduceArray = arr.reduce((acc, current) => {
acc.push(current * 2)
return acc
}, [])
console.log(mapArray, reduceArray) // [2, 4, 6]
18.你理解的 Generator 是什么?
Generator 算是 ES6 中难理解的概念之一了,Generator 最大的特点就是可以控制函数的执行。在这一小节中我们不会去讲什么是 Generator,而是把重点放在 Generator 的一些容易困惑的地方。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
你也许会疑惑为什么会产生与你预想不同的值,接下来逐行代码分析原因
- 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
- 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
- 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
- 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
Generator 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
更多参考资料请看 ES6新特性:Javascript中Generator(生成器)
19.async 及 await 的特点,它们的优点和缺点分别是什么?await 原理是什么?
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}
async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用。
async 和 await 可以说是异步终极解决方案了,相比直接使用 Promise 来说,优势在于处理 then 的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}
下面来看一个使用 await 的例子:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
- 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
- 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10