Tiny'Wo | 小窝

网络中的一小块自留地

面试官:如何使用css完成视差滚动效果?

一、是什�?

视差滚动(Parallax Scrolling)是指多层背景以不同的速度移动,形成立体的运动效果,带来非常出色的视觉体验

我们可以把网页解刨成:背景层、内容层、悬浮层

当滚动鼠标滑轮的时候,各个图层以不同的速度移动,形成视觉差的效�?
image.png

二、实现方�?

使用css形式实现视觉差滚动效果的方式有:

  • background-attachment
  • transform:translate3D

background-attachment

作用是设置背景图像是否固定或者随着页面的其余部分滚�?
值分别有如下�?

  • scroll:默认值,背景图像会随着页面其余部分的滚动而移�?- fixed:当页面的其余部分滚动时,背景图像不会移�?- inherit:继承父元素background-attachment属性的�?
    完成滚动视觉差就需要将background-attachment属性设置为fixed,让背景相对于视口固定。及时一个元素有滚动机制,背景也不会随着元素的内容而滚�?
    也就是说,背景一开始就已经被固定在初始的位�?
    核心的css代码如下�?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    section {
    height: 100vh;
    }

    .g-img {
    background-image: url(...);
    background-attachment: fixed;
    background-size: cover;
    background-position: center center;
    }

整体例子如下�?

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
<style>
div {
height: 100vh;
background: rgba(0, 0, 0, .7);
color: #fff;
line-height: 100vh;
text-align: center;
font-size: 20vh;
}

.a-img1 {
background-image: url(https://images.pexels.com/photos/1097491/pexels-photo-1097491.jpeg);
background-attachment: fixed;
background-size: cover;
background-position: center center;
}

.a-img2 {
background-image: url(https://images.pexels.com/photos/2437299/pexels-photo-2437299.jpeg);
background-attachment: fixed;
background-size: cover;
background-position: center center;
}

.a-img3 {
background-image: url(https://images.pexels.com/photos/1005417/pexels-photo-1005417.jpeg);
background-attachment: fixed;
background-size: cover;
background-position: center center;
}
</style>
<div class="a-text">1</div>
<div class="a-img1">2</div>
<div class="a-text">3</div>
<div class="a-img2">4</div>
<div class="a-text">5</div>
<div class="a-img3">6</div>
<div class="a-text">7</div>

transform:translate3D

同样,让我们先来看一下两个概念transformperspective�?

  • transform: css3 属性,可以对元素进行变�?2d/3d),包括平�?translate,旋转 rotate,缩放 scale,等等
  • perspective: css3 属性,当元素涉�?3d 变换时,perspective 可以定义我们眼睛看到�?3d 立体效果,即空间�?
    3D视角示意图如下所示:

举个例子�?

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
<style>
html {
overflow: hidden;
height: 100%
}

body {
/* 视差元素的父级需�?D视角 */
perspective: 1px;
transform-style: preserve-3d;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
#app{
width: 100vw;
height:200vh;
background:skyblue;
padding-top:100px;
}
.one{
width:500px;
height:200px;
background:#409eff;
transform: translateZ(0px);
margin-bottom: 50px;
}
.two{
width:500px;
height:200px;
background:#67c23a;
transform: translateZ(-1px);
margin-bottom: 150px;
}
.three{
width:500px;
height:200px;
background:#e6a23c;
transform: translateZ(-2px);
margin-bottom: 150px;
}
</style>
<div id="app">
<div class="one">one</div>
<div class="two">two</div>
<div class="three">three</div>
</div>

而这种方式实现视觉差动的原理如下�?

  • 容器设置�?transform-style: preserve-3d �?perspective: xpx,那么处于这个容器的子元素就将位�?D空间中,

  • 子元素设置不同的 transform: translateZ(),这个时候,不同元素�?3D Z轴方向距离屏幕(我们的眼睛)的距离也就不一�?

  • 滚动滚动条,由于子元素设置了不同�?transform: translateZ(),那么他们滚动的上下距离 translateY 相对屏幕(我们的眼睛),也是不一样的,这就达到了滚动视差的效�?

参考文�?

面试官:说说你对工厂模式的理解?应用场景�?

一、是什�?

工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工�?
其就像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品

举个例子�?

  • 编程中,在一�?A 类中通过 new 的方式实例化了类 B,那�?A 类和 B 类之间就存在关联(耦合�?- 后期因为需要修改了 B 类的代码和使用方式,比如构造函数中传入参数,那�?A 类也要跟着修改,一个类的依赖可能影响不大,但若有多个类依赖�?B 类,那么这个工作量将会相当的大,容易出现修改错误,也会产生很多的重复代码,这无疑是件非常痛苦的事�?- 这种情况下,就需要将创建实例的工作从调用方(A类)中分离,与调用方*解�?*,也就是使用工厂方法创建实例的工作封装起来(**减少代码重复*),由工厂管理对象的创建逻辑,调用方不需要知道具体的创建过程,只管使用,**而降低调用者因为创建逻辑导致的错�?*�?

二、实�?

工厂模式根据抽象程度的不同可以分为:

  • 简单工厂模式(Simple Factory�?- 工厂方法模式(Factory Method�?- 抽象工厂模式(Abstract Factory�?

简单工厂模�?

简单工厂模式也叫静态工厂模式,用一个工厂对象创建同一类对象类的实�?
假设我们要开发一个公司岗位及其工作内容的录入信息,不同岗位的工作内容不一�?
代码如下�?

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
function Factory(career) {
function User(career, work) {
this.career = career
this.work = work
}
let work
switch(career) {
case 'coder':
work = ['写代�?, 'Bug']
return new User(career, work)
break
case 'hr':
work = ['招聘', '员工信息管理']
return new User(career, work)
break
case 'driver':
work = ['开�?]
return new User(career, work)
break
case 'boss':
work = ['喝茶', '开�?, '审批文件']
return new User(career, work)
break
}
}
let coder = new Factory('coder')
console.log(coder)
let boss = new Factory('boss')
console.log(boss)

Factory就是一个简单工厂。当我们调用工厂函数时,只需要传递name、age、career就可以获取到包含用户工作内容的实例对�?

工厂方法模式

工厂方法模式跟简单工厂模式差不多,但是把具体的产品放到了工厂函数的prototype�?
这样一来,扩展产品种类就不必修改工厂函数了,和心累就变成抽象类,也可以随时重写某种具体的产�?
也就是相当于工厂总部不生产产品了,交给下辖分工厂进行生产;但是进入工厂之前,需要有个判断来验证你要生产的东西是否是属于我们工厂所生产范围,如果是,就丢给下辖工厂来进行生�?
如下代码�?

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
// 工厂方法
function Factory(career){
if(this instanceof Factory){
var a = new this[career]();
return a;
}else{
return new Factory(career);
}
}
// 工厂方法函数的原型中设置所有对象的构造函�?Factory.prototype={
'coder': function(){
this.careerName = '程序�?
this.work = ['写代�?, '修Bug']
},
'hr': function(){
this.careerName = 'HR'
this.work = ['招聘', '员工信息管理']
},
'driver': function () {
this.careerName = '司机'
this.work = ['开�?]
},
'boss': function(){
this.careerName = '老板'
this.work = ['喝茶', '开�?, '审批文件']
}
}
let coder = new Factory('coder')
console.log(coder)
let hr = new Factory('hr')
console.log(hr)

工厂方法关键核心代码是工厂里面的判断this是否属于工厂,也就是做了分支判断,这个工厂只做我能做的产�?

抽象工厂模式

上述简单工厂模式和工厂方法模式都是直接生成实例,但是抽象工厂模式不同,抽象工厂模式并不直接生成实例�?而是用于对产品类簇的创建

通俗点来讲就是:简单工厂和工厂方法模式的工作是生产产品,那么抽象工厂模式的工作就是生产工厂�?
由于JavaScript中并没有抽象类的概念,只能模拟,可以分成四部分:

  • 用于创建抽象类的函数
  • 抽象�?- 具体�?- 实例化具体类

上面的例子中有coderhrbossdriver四种岗位,其中coder可能使用不同的开发语言进行开发,比如JavaScriptJava等等。那么这两种语言就是对应的类�?
示例代码如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let CareerAbstractFactory = function(subType, superType) {
// 判断抽象工厂中是否有该抽象类
if (typeof CareerAbstractFactory[superType] === 'function') {
// 缓存�? function F() {}
// 继承父类属性和方法
F.prototype = new CareerAbstractFactory[superType]()
// 将子类的constructor指向父类
subType.constructor = subType;
// 子类原型继承父类
subType.prototype = new F()
} else {
throw new Error('抽象类不存在')
}
}

上面代码中CareerAbstractFactory就是一个抽象工厂方法,该方法在参数中传递子类和父类,在方法体内部实现了子类对父类的继承

三、应用场�?

从上面可看到,简单简单工厂的优点就是我们只要传递正确的参数,就能获得所需的对象,而不需要关心其创建的具体细�?
应用场景也容易识别,有构造函数的地方,就应该考虑简单工厂,但是如果函数构建函数太多与复杂,会导致工厂函数变得复杂,所以不适合复杂的情�?
抽象工厂模式一般用于严格要求以面向对象思想进行开发的超大型项目中,我们一般常规的开发的话一般就是简单工厂和工厂方法模式会用的比较多一�?
综上,工厂模式适用场景如下�?

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行�?这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活�?

参考文�?

面试官:说说你对发布订阅、观察者模式的理解?区别?

一、观察者模�?

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯

例如生活中,我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报�?
报社和订报纸的客户就形成了一对多的依赖关�?
实现代码如下�?
被观察者模�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Subject {

constructor() {
this.observerList = [];
}

addObserver(observer) {
this.observerList.push(observer);
}

removeObserver(observer) {
const index = this.observerList.findIndex(o => o.name === observer.name);
this.observerList.splice(index, 1);
}

notifyObservers(message) {
const observers = this.observeList;
observers.forEach(observer => observer.notified(message));
}

}

观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Observer {

constructor(name, subject) {
this.name = name;
if (subject) {
subject.addObserver(this);
}
}

notified(message) {
console.log(this.name, 'got message', message);
}
}

使用代码如下�?

1
2
3
4
5
6
7
const subject = new Subject();
const observerA = new Observer('observerA', subject);
const observerB = new Observer('observerB');
subject.addObserver(observerB);
subject.notifyObservers('Hello from subject');
subject.removeObserver(observerA);
subject.notifyObservers('Hello again');

上述代码中,观察者主动申请加入被观察者的列表,被观察者主动将观察者加入列�?

二、发布订阅模�?

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存�?

实现代码如下�?

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
class PubSub {
constructor() {
this.messages = {};
this.listeners = {};
}
// 添加发布�? publish(type, content) {
const existContent = this.messages[type];
if (!existContent) {
this.messages[type] = [];
}
this.messages[type].push(content);
}
// 添加订阅�? subscribe(type, cb) {
const existListener = this.listeners[type];
if (!existListener) {
this.listeners[type] = [];
}
this.listeners[type].push(cb);
}
// 通知
notify(type) {
const messages = this.messages[type];
const subscribers = this.listeners[type] || [];
subscribers.forEach((cb, index) => cb(messages[index]));
}
}

发布者代码如下:

1
2
3
4
5
6
7
8
9
class Publisher {
constructor(name, context) {
this.name = name;
this.context = context;
}
publish(type, content) {
this.context.publish(type, content);
}
}

订阅者代码如下:

1
2
3
4
5
6
7
8
9
class Subscriber {
constructor(name, context) {
this.name = name;
this.context = context;
}
subscribe(type, cb) {
this.context.subscribe(type, cb);
}
}

使用代码如下�?

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
const TYPE_A = 'music';
const TYPE_B = 'movie';
const TYPE_C = 'novel';

const pubsub = new PubSub();

const publisherA = new Publisher('publisherA', pubsub);
publisherA.publish(TYPE_A, 'we are young');
publisherA.publish(TYPE_B, 'the silicon valley');
const publisherB = new Publisher('publisherB', pubsub);
publisherB.publish(TYPE_A, 'stronger');
const publisherC = new Publisher('publisherC', pubsub);
publisherC.publish(TYPE_C, 'a brief history of time');

const subscriberA = new Subscriber('subscriberA', pubsub);
subscriberA.subscribe(TYPE_A, res => {
console.log('subscriberA received', res)
});
const subscriberB = new Subscriber('subscriberB', pubsub);
subscriberB.subscribe(TYPE_C, res => {
console.log('subscriberB received', res)
});
const subscriberC = new Subscriber('subscriberC', pubsub);
subscriberC.subscribe(TYPE_B, res => {
console.log('subscriberC received', res)
});

pubsub.notify(TYPE_A);
pubsub.notify(TYPE_B);
pubsub.notify(TYPE_C);

上述代码,发布者和订阅者需要通过发布订阅中心进行关联,发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责

三、区�?

两种设计模式思路是一样的,举个生活例子:

  • 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是“公司”和“员工”是一个整�?- 发布-订阅模式:某公司要给其他人发各种快递,因为“公司”和“其他人”是独立的,其唯一的桥梁是“快递”,所以这件事适合交给第三方快递公司解�?
    上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合�?
    两者区别如下图�?

  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信�?

  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反�?

  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发�?订阅模式大多数时候是异步的(使用消息队列�?

参考文�?

面试官:说说你对代理模式的理解?应用场景�?

一、是什�?

代理模式(Proxy Pattern)是为一个对象提供一个代用品或占位符,以便控制对它的访问

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要时,提供一个替身对象来控制这个对象的访问,客户实际上访问的是替身对�?

在生活中,代理模式的场景是十分常见的,例如我们现在如果有租房、买房的需求,更多的是去找链家等房屋中介机构,而不是直接寻找想卖房或出租房的人谈。此时,链家起到的作用就是代理的作用

二、使�?

ES6中,存在proxy构建函数能够让我们轻松使用代理模式:

1
const proxy = new Proxy(target, handler);

关于Proxy的使用可以翻看以前的文章

而按照功能来划分,javascript代理模式常用的有�?

  • 缓存代理

  • 虚拟代理

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果

如实现一个求积乘的函数,如下�?

1
2
3
4
5
6
7
8
var muti = function () {
console.log("开始计算乘�?);
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
};

现在加入缓存代理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var proxyMult = (function () {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ",");
if (args in cache) {
return cache[args];
}
return (cache[args] = mult.apply(this, arguments));
};
})();

proxyMult(1, 2, 3, 4); // 输出:24
proxyMult(1, 2, 3, 4); // 输出:24

当第二次调用 proxyMult(1, 2, 3, 4) 时,本体 mult 函数并没有被计算,proxyMult 直接返回了之前缓存好的计算结�?

虚拟代理

虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创�?
常见的就是图片预加载功能�?
未使用代理模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let MyImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;

img.onload = function(){
// 监听到图片加载完成后,设置src为加载完成后的图�? imgNode.src = img.src;
};

return {
setSrc: function( src ){
// 设置图片的时候,设置为默认的loading�? imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif';
// 把真正需要设置的图片传给Image对象的src属�? img.src = src;
}
}
})();

MyImage.setSrc( 'https://xxx.jpg' );

MyImage对象除了负责给img节点设置src外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则

上述过程loding则是耦合进MyImage对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage对象里面改动代码

使用代理模式,代码则如下�?

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
// 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口
let myImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );

return {
//setSrc接口,外界调用这个接口,便可以给该img标签设置src属�? setSrc: function( src ){
imgNode.src = src;
}
}
})();
// 代理对象,负责图片预加载功能
let proxyImage = (function(){
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;
img.onload = function(){
// 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图�? myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
// 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加�? myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' );
img.src = src;
}
}
})();

proxyImage.setSrc( 'https://xxx.jpg' );

使用代理模式后,图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口�?
代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功�?
上述并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为

并且上述代理模式可以发现,代理和本体接口的一致性,如果有一天不需要预加载,那么就不需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供�?setSrc 方法

�?

三、应用场�?

现在的很多前端框架或者状态管理框架都使用代理模式,用与监听变量的变化

使用代理模式代理对象的访问的方式,一般又被称为拦截器,比如我们在项目中经常使�?Axios 的实例来进行 HTTP 的请求,使用拦截�?interceptor 可以提前�?请求前的数据 服务器返回的数据进行一些预处理

以及上述应用到的缓存代理和虚拟代�?

参考文�?

面试官:说说你对单例模式的理解?如何实现�?

一、是什�?

单例模式(Singleton Pattern):创建型模式,提供了一种创建对象的最佳方式,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建

在应用程序运行期间,单例模式只会在全局作用域下创建一次实例对象,让所有需要调用的地方都共享这一单例对象,如下图所示:

从定义上来看,全局变量好像就是单例模式,但是一般情况我们不认为全局变量是一个单例模式,原因是:

  • 全局命名污染
  • 不易维护,容易被重写覆盖

二、实�?

javascript中,实现一个单例模式可以用一个变量来标志当前的类已经创建过对象,如果下次获取当前类的实例时,直接返回之前创建的对象即可,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义一个类
function Singleton(name) {
this.name = name;
this.instance = null;
}
// 原型扩展类的一个方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 获取类的实例
Singleton.getInstance = function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance
};

// 获取对象1
const a = Singleton.getInstance('a');
// 获取对象2
const b = Singleton.getInstance('b');
// 进行比较
console.log(a === b);

使用闭包也能够实现,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Singleton(name) {
this.name = name;
}
// 原型扩展类的一个方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 获取类的实例
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance
}
})();

// 获取对象1
const a = Singleton.getInstance('a');
// 获取对象2
const b = Singleton.getInstance('b');
// 进行比较
console.log(a === b);

也可以将上述的方法稍作修改,变成构造函数的形式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 单例构造函�?  function CreateSingleton (name) {
this.name = name;
this.getName();
};

// 获取实例的名�? CreateSingleton.prototype.getName = function() {
console.log(this.name)
};
// 单例对象
const Singleton = (function(){
var instance;
return function (name) {
if(!instance) {
instance = new CreateSingleton(name);
}
return instance;
}
})();

// 创建实例对象1
const a = new Singleton('a');
// 创建实例对象2
const b = new Singleton('b');

console.log(a===b); // true

三、使用场�?

在前端中,很多情况都是用到单例模式,例如页面存在一个模态框的时候,只有用户点击的时候才会创建,而不是加载完成之后再创建弹窗和隐藏,并且保证弹窗全局只有一�?
可以先创建一个通常的获取对象的方法,如下:

1
2
3
4
5
6
const getSingle = function( fn ){
let result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};

创建弹窗的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const createLoginLayer = function(){
var div = document.createElement( 'div' );
div.innerHTML = '我是浮窗';
div.style.display = 'none';
document.body.appendChild( div );
return div;
};

const createSingleLoginLayer = getSingle( createLoginLayer );

document.getElementById( 'loginBtn' ).onclick = function(){
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};

上述这种实现称为惰性单例,意图解决需要时才创建类实例对象

并且Vuexredux全局态管理库也应用单例模式的思想,如下图�?

现在很多第三方库都是单例模式,多次引用只会使用同一个对象,如jquerylodashmoment

参考文�?

面试官:说说你对策略模式的理解?应用场景�?

一、是什�?

策略模式(Strategy Pattern)指的是定义一系列的算法,把它们一个个封装起来,目的就是将算法的使用与算法的实现分离开�?
一个基于策略模式的程序至少由两部分组成�?

  • 策略类,策略类封装了具体的算法,并负责具体的计算过程
  • 环境类Context,Context 接受客户的请求,随后 把请求委托给某一个策略类

二、使�?

举个例子,公司的年终奖是根据员工的工资和绩效来考核的,绩效为A的人,年终奖为工资的4倍,绩效为B的人,年终奖为工资的3倍,绩效为C的人,年终奖为工资的2�?
若使用if来实现,代码则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var calculateBouns = function(salary,level) {
if(level === 'A') {
return salary * 4;
}
if(level === 'B') {
return salary * 3;
}
if(level === 'C') {
return salary * 2;
}
};
// 调用如下�?console.log(calculateBouns(4000,'A')); // 16000
console.log(calculateBouns(2500,'B')); // 7500

从上述可有看到,函数内部包含过多if...else,并且后续改正的时候,需要在函数内部添加逻辑,违反了开放封闭原�?
而如果使用策略模式,就是先定义一系列算法,把它们一个个封装起来,将不变的部分和变化的部分隔开,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
"A": function(salary) {
return salary * 4;
},
"B" : function(salary) {
return salary * 3;
},
"C" : function(salary) {
return salary * 2;
}
};
var calculateBouns =function(level,salary) {
return obj[level](salary);
};
console.log(calculateBouns('A',10000)); // 40000

上述代码中,obj对应的是策略类,而calculateBouns对应上下通信�?
又比如实现一个表单校验的代码,常常会像如下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
if(registerForm.userName.value === '') {
alert('用户名不能为�?);
return;
}
if(registerForm.password.value.length < 6) {
alert("密码的长度不能小�?�?);
return;
}
if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
alert("手机号码格式不正�?);
return;
}
}

上述代码包含多处if语句,并且违反了开放封闭原则,如果应用中还有其他的表单,需要重复编写代�?
此处也可以使用策略模式进行重构校验,第一步确定不变的内容,即策略规则对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var strategy = {
isNotEmpty: function(value,errorMsg) {
if(value === '') {
return errorMsg;
}
},
// 限制最小长�? minLength: function(value,length,errorMsg) {
if(value.length < length) {
return errorMsg;
}
},
// 手机号码格式
mobileFormat: function(value,errorMsg) {
if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};

然后找出变的地方,作为环境类context,负责接收用户的要求并委托给策略规则对象,如下Validator类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Validator = function(){
this.cache = []; // 保存效验规则
};
Validator.prototype.add = function(dom,rule,errorMsg) {
var str = rule.split(":");
this.cache.push(function(){
// str 返回的是 minLength:6
var strategy = str.shift();
str.unshift(dom.value); // 把input的value添加进参数列�? str.push(errorMsg); // 把errorMsg添加进参数列�? return strategys[strategy].apply(dom,str);
});
};
Validator.prototype.start = function(){
for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) {
var msg = validatorFunc(); // 开始效�?并取得效验后的返回信�? if(msg) {
return msg;
}
}
};

通过validator.add方法添加校验规则和错误信息提示,使用如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var validateFunc = function(){
var validator = new Validator(); // 创建一个Validator对象
/* 添加一些效验规�?*/
validator.add(registerForm.userName,'isNotEmpty','用户名不能为�?);
validator.add(registerForm.password,'minLength:6','密码长度不能小于6�?);
validator.add(registerForm.userName,'mobileFormat','手机号码格式不正�?);

var errorMsg = validator.start(); // 获得效验结果
return errorMsg; // 返回效验结果
};
var registerForm = document.getElementById("registerForm");
registerForm.onsubmit = function(){
var errorMsg = validateFunc();
if(errorMsg){
alert(errorMsg);
return false;
}
}

上述通过策略模式完成表单的验证,并且可以随时调用,在修改表单验证规则的时候,也非常方便,通过传递参数即可调�?

三、应用场�?

从上面可以看到,使用策略模式的优点有如下�?

  • 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句
  • 策略模式提供了开�?封闭原则,使代码更容易理解和扩展
  • 策略模式中的代码可以复用

策略模式不仅仅用来封装算法,在实际开发中,通常会把算法的含义扩散开来,使策略模式也可以用来封装 一系列的“业务规则�?
只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它�?

参考文�?

面试官:说说对设计模式的理解?常见的设计模式有哪些?

一、是什�?

在软件工程中,设计模式是对软件设计中普遍存在的各种问题所提出的解决方�?
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方�?
设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能�?
因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模�?
比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文�?
因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题

二、有哪些

常见的设计模式有�?

  • 单例模式
  • 工厂模式
  • 策略模式
  • 代理模式
  • 中介者模�?- 装饰者模�?- ……

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对�?
如下图的车,只有一辆,一旦借出去则不能再借给别人�?

工厂模式

工厂模式通常会分�?个角色:

  • 工厂角色-负责实现创建所有实例的内部逻辑.
  • 抽象产品角色-是所创建的所有对象的父类,负责描述所有实例所共有的公共接�?- 具体产品角色-是创建目标,所有创建的对象都充当这个角色的某个具体类的实例

策略模式

策略模式,就是定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换

至少分成两部分:

  • 策略类(可变),策略类封装了具体的算法,并负责具体的计算过程
  • 环境类(不变),接受客户的请求,随后将请求委托给某一个策略类

代理模式

代理模式:为对象提供一个代用品或占位符,以便控制对它的访问

例如实现图片懒加载的功能,先通过一张loading图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到img标签里面

中介者模�?

中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即�?
通过中介者模式可以解除对象与对象之间的紧耦合关系

装饰者模�?

装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法

通常运用在原有方法维持不变,在原有方法上再挂载其他方法来满足现有需�?

三、总结

不断去学习设计模式,会对我们有着极大的帮助,主要如下�?

  • 从许多优秀的软件系统中总结出的成功的、能够实现可维护性、复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作

  • 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂

  • 大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码

  • 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统

  • 学习设计模式将有助于初学者更加深入地理解面向对象思想

参考文�?

面试官:ES6中数组新增了哪些扩展�?

一、扩展运算符的应�?

ES6通过扩展元素符...,好�?rest 参数的逆运算,将一个数组转为用逗号分隔的参数序�?

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

主要用于函数调用的时候,将一个数组变为参数序�?

1
2
3
4
5
6
7
8
9
10
function push(array, ...items) {
array.push(...items);
}

function add(x, y) {
return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

可以将某些数据结构转为数�?

1
[...document.querySelectorAll('div')]

能够更简单实现数组复�?

1
2
3
const a1 = [1, 2];
const [...a2] = a1;
// [1,2]

数组的合并也更为简洁了

1
2
3
4
5
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数�?
下面看个例子就清楚多�?

1
2
3
4
const arr1 = ['a', 'b',[1,2]];
const arr2 = ['c'];
const arr3 = [...arr1,...arr2]
arr[1][0] = 9999 // 修改arr1里面数组成员�?console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c']

扩展运算符可以与解构赋值结合起来,用于生成数组

1
2
3
4
5
6
7
8
9
10
11
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest // []

const [first, ...rest] = ["foo"];
first // "foo"
rest // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报�?

1
2
3
4
5
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

可以将字符串转为真正的数�?

1
2
[...'hello']
// [ "h", "e", "l", "l", "o" ]

定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数�?

1
2
3
4
5
6
7
8
9
10
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];

let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

如果对没�?Iterator 接口的对象,使用扩展运算符,将会报错

1
2
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object

二、构造函数新增的方法

关于构造函数,数组新增的方法有如下�?

  • Array.from()
  • Array.of()

Array.from()

将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结�?Set �?Map�?

1
2
3
4
5
6
7
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组

1
2
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

Array.of()

用于将一组值,转换为数�?

1
Array.of(3, 11, 8) // [3,11,8]

没有参数的时候,返回一个空数组

当参数只有一个的时候,实际上是指定数组的长�?
参数个数不少�?2 个时,Array()才会返回由参数组成的新数�?

1
2
3
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

三、实例对象新增的方法

关于数组实例对象新增的方法有如下�?

  • copyWithin()
  • find()、findIndex()
  • fill()
  • entries(),keys(),values()
  • includes()
  • flat(),flatMap()

copyWithin()

将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数�?
参数如下�?

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数�?- start(可选):从该位置开始读取数据,默认�?0。如果为负值,表示从末尾开始计算�?- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算�?
    1
    2
    [1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 �?5),复制到从 0 号位开始的位置,结果覆盖了原来�?1 �?2
    // [4, 5, 3, 4, 5]

find()、findIndex()

find()用于找出第一个符合条件的数组成员

参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组

1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

findIndex返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

1
2
3
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象�?

1
2
3
4
5
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26

fill()

使用给定值,填充一个数�?

1
2
3
4
5
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位�?

1
2
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

注意,如果填充的类型为对象,则是浅拷�?

entries(),keys(),values()

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
or (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"

includes()

用于判断数组是否包含给定的�?

1
2
3
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true

方法的第二个参数表示搜索的起始位置,默认为0

参数为负数则表示倒数的位�?

1
2
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

flat(),flatMap()

将数组扁平化处理,返回一个新数组,对原数据没有影�?

1
2
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认�?

1
2
3
4
5
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

flatMap()方法对原数组的每个成员执行一个函数相当于执行Array.prototype.map(),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数�?

1
2
3
// 相当�?[[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

四、数组的空位

数组的空位指,数组的某一个位置没有任何�?
ES6 则是明确将空位转为undefined,包括Array.from、扩展运算符、copyWithin()fill()entries()keys()values()find()findIndex()

建议大家在日常书写中,避免出现空�?

五、排序稳定�?

sort()默认设置为稳定的排序算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr = [
'peach',
'straw',
'apple',
'spork'
];

const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};

arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]

排序结果中,strawspork的前面,跟原始顺序一�?

参考文�?

面试官:你是怎么理解ES6�?Decorator 的?使用场景�?

一、介�?

Decorator,即装饰器,从名字上很容易让我们联想到装饰者模�?
简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论�?
ES6Decorator功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方�?
这里定义一个士兵,这时候他什么装备都没有

1
2
class soldier{ 
}

定义一个得�?AK 装备的函数,即装饰器

1
2
3
function strong(target){
target.AK = true
}

使用该装饰器对士兵进行增�?

1
2
3
@strong
class soldier{
}

这时候士兵就有武器了

1
soldier.AK // true

上述代码虽然简单,但也能够清晰看到了使用Decorator两大优点�?

  • 代码可读性变强了,装饰器命名相当于一个注�?- 在不改变原有代码情况下,对原来功能进行扩�?

二、用�?

Docorator修饰对象为下面两种:

  • 类的装饰
  • 类属性的装饰

类的装饰

当对类本身进行装饰的时候,能够接受一个参数,即类本身

将装饰器行为进行分解,大家能够有个更深入的了�?

1
2
3
4
5
6
@decorator
class A {}

// 等同�?
class A {}
A = decorator(A) || A;

下面@testable就是一个装饰器,target就是传入的类,即MyTestableClass,实现了为类添加静态属�?

1
2
3
4
5
6
7
8
9
10
@testable
class MyTestableClass {
// ...
}

function testable(target) {
target.isTestable = true;
}

MyTestableClass.isTestable // true

如果想要传递参数,可以在装饰器外层再封装一层函�?

1
2
3
4
5
6
7
8
9
10
11
12
13
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

类属性的装饰

当对类属性进行装饰的时候,能够接受三个参数�?

  • 类的原型对象
  • 需要装饰的属性名
  • 装饰属性名的描述对�?
    首先定义一个readonly装饰�?
    1
    2
    3
    4
    function readonly(target, name, descriptor){
    descriptor.writable = false; // 将可写属性设为false
    return descriptor;
    }

使用readonly装饰类的name方法

1
2
3
4
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}

相当于以下调�?

1
readonly(Person.prototype, 'name', descriptor);

如果一个方法有多个装饰器,就像洋葱一样,先从外到内进入,再由内到外执�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) =>console.log('executed', id);
}

class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

外层装饰器@dec(1)先进入,但是内层装饰器@dec(2)先执�?

注意

装饰器不能用于修饰函数,因为函数存在变量声明情况

1
2
3
4
5
6
7
8
9
var counter = 0;

var add = function () {
counter++;
};

@add
function foo() {
}

编译阶段,变成下�?

1
2
3
4
5
6
7
8
9
10
11
12
var counter;
var add;

@add
function foo() {
}

counter = 0;

add = function () {
counter++;
};

意图是执行后counter等于 1,但是实际上结果是counter等于 0

三、使用场�?

基于Decorator强大的作用,我们能够完成各种场景的需求,下面简单列举几种:

使用react-redux的时候,如果写成下面这种形式,既不雅观也很麻�?

1
2
3
class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

通过装饰器就变得简洁多�?

1
2
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

mixins,也可以写成装饰器,让使用更为简洁了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}

// 使用
const Foo = {
foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"

下面再讲讲core-decorators.js几个常见的装饰器

@antobind

autobind装饰器使得方法中的this对象,绑定原始对�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { autobind } from 'core-decorators';

class Person {
@autobind
getPerson() {
return this;
}
}

let person = new Person();
let getPerson = person.getPerson;

getPerson() === person;
// true

@readonly

readonly装饰器使得属性或方法不可�?

1
2
3
4
5
6
7
8
9
10
import { readonly } from 'core-decorators';

class Meal {
@readonly
entree = 'steak';
}

var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]

@deprecate

deprecatedeprecated装饰器在控制台显示一条警告,表示该方法将废除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { deprecate } from 'core-decorators';

class Person {
@deprecate
facepalm() {}

@deprecate('功能废除�?)
facepalmHard() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: 功能废除�?

参考文�?

面试官:对象新增了哪些扩展?

一、参�?

ES6允许为函数的参数设置默认�?

1
2
3
4
5
6
7
function log(x, y = 'World') {
console.log(x, y);
}

console.log('Hello') // Hello World
console.log('Hello', 'China') // Hello China
console.log('Hello', '') // Hello

函数的形参是默认声明的,不能使用letconst再次声明

1
2
3
4
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}

参数默认值可以与解构赋值的默认值结合起来使�?

1
2
3
4
5
6
7
8
function foo({x, y = 5}) {
console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined

上面的foo函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量xy就不会生成,从而报错,这里设置默认值避�?

1
2
3
4
5
function foo({x, y = 5} = {}) {
console.log(x, y);
}

foo() // undefined 5

参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没发省略�?

1
2
3
4
5
6
7
8
function f(x = 1, y) {
return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

二、属�?

函数的length属�?

length将返回没有指定默认值的参数个数

1
2
3
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

rest 参数也不会计入length属�?

1
(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了

1
2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

name属�?

返回该函数的函数�?

1
2
3
4
5
6
7
var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

如果将一个具名函数赋值给一个变量,�?name属性都返回这个具名函数原本的名�?

1
2
const bar = function baz() {};
bar.name // "baz"

Function构造函数返回的函数实例,name属性的值为anonymous

1
(new Function).name // "anonymous"

bind返回的函数,name属性值会加上bound前缀

1
2
3
4
function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

三、作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用�?
等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现�?
下面例子中,y=x会形成一个单独作用域,x没有被定义,所以指向全局变量x

1
2
3
4
5
6
7
8
9
let x = 1;

function f(y = x) {
// 等同�?let y = x
let x = 2;
console.log(y);
}

f() // 1

四、严格模�?

只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}

// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};

// 报错
const doSomething = (...a) => {
'use strict';
// code
};

const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};

五、箭头函�?

使用“箭头”(=>)定义函�?

1
2
3
4
5
var f = v => v;

// 等同�?var f = function (v) {
return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分

1
2
3
4
5
6
7
var f = () => 5;
// 等同�?var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同�?var sum = function(num1, num2) {
return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回

1
var sum = (num1, num2) => { return num1 + num2; }

如果返回对象,需要加括号将对象包�?

1
let getTempItem = id => ({ id: id, name: "Temp" });

注意点:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错�?- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以�?rest 参数代替
  • 不可以使用yield命令,因此箭头函数不能用�?Generator 函数

参考文�?- https://es6.ruanyifeng.com/#docs/function

0%