Willong's Blog


  • Home

  • About

  • Tags

  • Archives

JS的原型是什么鬼?

Posted on 2020-02-23

其实作为一个入行那么久的前端,不应该在这时候才来写这种文章了。不过既然之前没写,今天就来总计一下。

在说原型之前,得先说一下JS关于继承的实现。其实JS的继承跟其他Java或者C++等面向对象实现的继承不一样,JS的继承是通过原型的继承来实现的。也就是说JS的继承是另一种方法。

提到JS的原型,及绕不开三个概念:

  • constructor/构造函数,
  • prototype/原型,
  • proto/原型链。

constructor 构造函数(实例生产者)

构造函数,概念跟面向对象的构造函数一个概念。粗略地说就是“用来产生实例的函数。”每个类都会有一个构造函数。

而在JS当中,所有的函数都是构造函数,只要当它跟new关键字一起使用时,就起到了构造函数的作用。

prototype 原型(模板)

prototype 是每个构造函数产生的时候都会同时产生的一个对象,里面有 constructor 属性,指向函数本身,而函数本身也会有一个prototype属性指向prototype。

我个人理解,用通俗的说法就行,每个函数的产生,都会有一个对应的数据结构产生,这个东西就叫prototype(原型),相当于一个模板。每次new一个实例的时候,都会从这个原型上进行一次copy,然后将复制体返回给外面。

所以,当我们在某个function的prototype上挂载方法或者属性时,我们实际上是修改了这个“类”(为了方便理解姑且这么叫吧)的模板。那既然模板都被修改了,所以所有生产出来的实例都会跟着变化啦!

__proto__ 原型链(模板是谁)

既然知道了原型,那这个原型链又是什么东西?

原型链属于实例(相对应的,实例是没有prototype的,只有构造函数才有),用于指向这个实例的构造函数的原型。如果画一个图的话,那么我觉得这个proto可以作为一条线,然后连接实例和它的构造函数的prototype。

也正是有了proto,实例才能够使用prototype上面的属性和方法,否则实例的成员只包含构造函数内声明的属性或者方法。


其实概念就这么简单。只要将上面的几个概念记牢了,面试时面对一些提问的方法,加以套用就能答出正解。

todo 补充一些变体问题

面试记@YY

Posted on 2020-01-26

由于放假前收到YY电商部门的面试邀请,抱着试一试的心态接收了面试邀请。最终结果应该是凉凉了,但是还是有所得,趁着过年期间进行一下总结。

一面:技术面

是两位前端技术人员进行面试,此时气氛比较轻松,聊到的东西也比较广,有几个问题比较深刻。

  1. 因为简历上写到一个Git的子模块的使用,询问了一下子模块的使用,Git的工作流,为什么不用私有npm(其实后来有搭)

  2. 现在有一个类似webpack那样的项目(我的解读是有一个核心功能+各种loader/plugin),会怎么对这个项目进行代码管理?

    1. 目录怎样划分?
    2. 内置的一些功能(loader/plugin)怎么划分?
  3. 如果有一个大型项目(类似内部到处用的脚手架那种),如果这个大包的线上版本有问题,其所依赖的一个包是有问题的,怎么去解决这个问题?我的回答是,这个项目在设计之初,就要提供一个“自检”的阶段

  4. 说一下VUE的特性,双向绑定的原理(粗略带过),提到Object.defineProperity,依赖收集等等,其实是比较含糊的

  5. socket和websocket是一样吗?(直接说不清楚,大概说了一下是长连接),还提了一下Web-CRT是什么,跟web-socket有啥不一样?

  6. 提到了一个之前被DNS劫持的,怎么解决的。页面被嵌套iframe。展开发问,遇到DNS劫持,有什么办法解决。

  7. http 1.0、1.1、2.0 之间的区别?

  8. 有一个实时性需要很高且很高并发的列表,在node-SSR的项目中,怎么维持这个列表的实施性?

二面:技术面(组长/负责人 面)

  1. VUE 的双向绑定怎么实现,需要非常详细说,整个源码级别的理解。
    1. 依赖收集是怎么实现的
    2. 动态化
  2. Vue的生命周期,只是简单地说了有那些周期,没有述说他们的区别,也是做得不够好的地方。
  3. 画出构造函数+原型之间+实例的关系。关于原型链的一个普通问题,没有答出来,其实很不应该。
  4. 从2展开的一个问题,有一个A的function(内含一个字段+一个方法),现在B想要继承A,直接functionB去call一下A。同样要求描述一下B跟题目2中间的关系链条,也是没有答出来
  5. 说一下对docker的了解

三面:HR面

  1. 为什么想离开原有的公司?
  2. 觉得自己是一个怎样的人?
  3. 希望去到一个怎样的环境/团队?

总结

  1. http 各个版本的区别确实基本上等于空白,只知道http协议的请求头+请求体,缓存控制字段
  2. Vue的原理不够清晰,表达含糊
  3. JS的原型和构造函数等几个之间的关系

Dart语言入门

Posted on 2019-12-26

之前在微信文章评论点赞收到了一本Flutter入门与实战,于是以此为契机来学习一把Flutter的技术栈。这篇文章主要是介绍Dart语言的一些基本语法。

1. 变量与基本数据类型

声明操作:

1
2
3
4
var name='小明';`

var name2 // 未赋值默认值是null
if(name2 == null) // true

1.1 常量和固定值

以 final const 关键字声明:

1
2
3
4
5
6
7
8
final username = '张三'; // final 的值只能被设定一次
username = '李四'; // 会报错

const pi = 3.1415926;
const area = pi*100*100;

final stars = const []; // const 关键字可以作为构造函数创建常量
const buttons = const [];

1.2 基本数据类型

Dart 常用基本数据类型包括:Number、String、Boolean、List、Map

1.2.1 Number类型

Number下面还包括int(整型)和double(浮点)

  • 整型:取值范围-2^53 ~ 2^53
  • 浮点型:64为长度的浮点型数据,双精度浮点型

基本操作四则运算(+ - * /)和位移操作 >>.

常用方法:abs、ceil、floot。

1.2.2 String 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
var str = '字符串哈哈哈'
var str2 = "双引号也行"
var str3 = str +str2 // 合并字符串

var multi_line_string1 = '''这个是一个
多行的
文本
类似
JS的 ``'''
var multi_line_string1 = """这个是一个
双引号
也
可以"""

1.2.3 Boolean 类型

Boolean 是true or flase 的类型。在Dart当中,没有隐式转换,只有真的是true的bool类型才是这真的 true。

1
2
3
4
var sex = 'male'
if(sex) { // 实际上在编译的时候会报类型错误
print('this people is a man') // 不会执行
}

1.2.4 List 类型

具有一系列相同类型的数据,称为List对象。类似于JS中的Array(不过JS的Array没要求内容一定要一样就是了)。索引那些也都一样,不多说了。

1
2
3
var list1 = [1,2,3];
print(list1.length); // 3
print(list1[0]); // 1

算法学习之题目记录

Posted on 2019-10-10

开坑声明

之前买了极客时间的一个《算法面试通关40讲》的课程,里面有提到的题目,打算以笔记的形式记录下来,一个题目的思考分析过程和自己的实现结果。

废话不多说,马上开始。


第一题:链表反转

题目描述:反转一个有序单链表。

1
2
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

解题思路

像视频里面说的,链表的这个题目思路或者说肉眼看上去就感觉比较简单。就是将链表里面的指针都反转一次。

步骤细化:

  1. 将大问题分成若干个小问题,整个链表的反转,实际上是每一个链表节点的next指针从本来指向的元素改成上一个元素。
  2. 替换指针,
    1. 理解当前节点cur、当前节点的上一个节点prev、当前节点的下一个节点nextNode,三个概念和对应的临时变量
    2. 先将cur的next存起来,准备推进流程用
    3. 修改cur.next = prev
    4. prev 已经被用掉了,所以要更新 prev = cur,当前节点变成了别人的prev
    5. 最后更新cur = 第2步存的nextNode,将原来的下一个节点推为新的cur
  3. 边界情况,当前节点没有next指针了

实际上我自己写的时候,是有问题的,没有用到prev这个临时变量去存储上一个的指针,自以为JS的引用类型,直接将cur和nextNode的next递归去修改,也没想着利用最后一个节点必然是null的情况去去占位prev。

下面是我在力扣上提交的代码,还有改进的空间。现在用的是while写的,还可以用递归去写一个版本,看下这周有没有时间去做。

我的代码

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
// 初次成功版
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/

/**
* @param {ListNode} head
* @return {ListNode}
*/

reverseList = function(head) {
// if(head.next === null) {
// // handle too short
// return head
// }

let prev = null,
cur = head,
nextNum,
temp
while(cur) {
nextNum = cur.next
cur.next = prev
prev = cur
cur = nextNum
nextNum = nextNum? nextNum.next: null
}
return prev
};

reverseList()
----- 第一题:链表反转 到此结束 -----

从前端角度学Docker

Posted on 2019-10-08

一、什么是Docker?

1.1 Docker这件事的前世今生 / Docker解决了什么问题 ?

要搞清楚Docker是什么东西,最简单的切入点,就是了解他解决了什么问题,如果一个东西没有解决任何问题,那么我们肯定没有理解和学习它的必要。那么Docker现在这么火,倒推过来,就表示Docker解决了很多人的痛点!

那么Docker解决了什么痛点呢?

就是软件开发当中的环境配置问题,学习软件开发的第一节课,可能都是如何配置XXX语言的运行环境,XXX框架的开发环境。这还是开发阶段的,以前在开发阶段没问题,去到部署的时候可能有问题,这种就是开发机和线上机的环境不一致造成的。为了维持软件运行环境所安装的软件依赖的一致性,给开发人员造成了很大的工作量。后来开发人员就希望要是能够保持环境的依赖一直一样就好了。于是出现了第一代方案:

1.1.1 虚拟机

虚拟机,就是在一台机器上再将资源分配划分为另一台机器。虚拟机以文件的形式存在于底层(物理)机器的操作系统上。虽然虚拟机可以通过以文件的形式承载一整个操作系统,也就解决了环境依赖的问题,但是虚拟机也有很大的缺点,就是:

  1. 占资源多
  2. 冗余步骤多,因为每次部署虚拟机都需要搞一次操作系统
  3. 启动慢

为了某个软件使用虚拟机方案,其实里面的系统就是这个方案的”副作用“。那么,能不能没有系统这一层副作用呢?答案是有的,就是Linux容器技术。

1.1.2 Linux容器

Linux容器(Linux Containers, AKA LXC)。
Linux容器不模拟一个完整的操作系统,只是对进程进行隔离。在容器里的进程,接触的系统资源都是被虚拟处理过的,类似做了一层物理资源的保护层,进行隔离。这样的操作是进程级别,相比虚拟机具备了其不具备的优点:

  1. 资源占用少
  2. 启动快
  3. 体积小

容器相当于进程级别的虚拟机,同样对系统资源进行虚拟化,但是没了操作系统这个拖油瓶,速度快了很多。

1.2 Docker是什么?

Docker是基于Linux容器的一种封装,提供简单易用的容器使用接口。是目前最流行的Linux容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

二、为什么要学习Docker?

http协议的缓存

Posted on 2019-10-03

之前一次面试被问到这方面的问题,对这方面进行了一次整理。废话不多说,直接列大纲。

  1. 前提:HTTP结构
  2. 缓存规则
  3. 缓存类型
  4. 强制缓存
  5. 对比缓存

前提:HTTP结构

HTTP请求,由请求头和请求体部分,请求体就是请求返回的我们要用的东西,请求头就是一些类似于配置的信息。

缓存规则/流程

浏览器内部有一个管理缓存的地方和对应的流程和规则,先按照网上的说法定义为浏览器的缓存数据库。

初次请求的时候,查询数据库,数据库返回没有数据,浏览器发起请求给浏览器,浏览器返回数据,将数据和缓存规则写入缓存系统
初次请求

下面是查询缓存数据库的时候发现强缓存规则时调用的规则和没有命中强缓存规则的流程。
强缓存规则

下面是查询缓存数据库的时候发现对比缓存规则时调用的规则和没有命中强缓存规则的流程。
对比缓存规则

  1. 发起请求,查询缓存仓库
    1. 有缓存,查询是否过期
    2. 没有缓存,发起请求,
  2. 查询缓存是否过期
    1. 没过期,走强制缓存路线
    2. 已经过期,查Etag
  3. Etag是否匹配
    1. 过期了

缓存类型:

  • 强制缓存
    • cache-control
    • expires
  • 对比缓存
    • last-Modified/If-Modified-Since
    • Etag/if-None-Match

强制缓存

强制缓存通过http请求的header部分标识,分别是Expires字段和Cache-control字段,下面来介绍一下这两者的作用。

Expires

Expires标识服务端返回的到期时间,在这个到期时间之前,资源都可以使用缓存,不用再问服务端索取。

Expires属性属于HTTP1.0的属性,现在浏览器默认都是HTTP1.1版本的协议,所以作用基本上已经可以忽略不计。

另外就是Expires的时间是服务器时间,如果客户端的时间跟服务端时间不同步,那么Expires基本上就跪了。比如我把电脑上的时间设置成10年后,那么无论我发送什么请求,都是缓存不了的,因为在我的客户端看来所有资源都是过期的。

Cache-Control

Cache-Control是强制缓存当前最常用的属性。上面说到Expires的服务器时间引起的过期问题,那就是说绝对时间不行,所以Cache-Control就改进了这一点,使用了相对时间。

Cache-Control的属性:

  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=xxx:资源在xxx秒后失效
  • no-cache:使用对比缓存来验证缓存数据
  • no-store:所有内容都不进行任何形式的缓存

对比缓存

什么是对比缓存:需要进行比较判断是否可以使用缓存。通俗地说,就是问下服务器,我能不能使用我本地的这一份缓存,如果可以就返回个304,不行就返回个200.

那问题来了,怎么去做这个问一下服务器的事情呢?主要就是依赖 Last-Modified/If-Modified-Since,E-tag/if-None-Match,这两块属性。

Last-Modified/If-Modified-Since

Last-Modified(返回):是服务器告诉浏览器,这个资源最后的修改时间。下次浏览器请求同一个资源的时候,会将这个资源的Last-Modified用If-Modified-Since的字段告诉服务器,服务器通过对比这个资源的最后修改时间,如果还是在Last-Modified的那个时间,那么就返回304告诉浏览器使用缓存。

If-Modified-Since(带过去):告诉服务器,服务器返回的资源最后的修改时间。

E-tag/If-None-Match

E-tag(返回):服务器响应的时候,告诉浏览器在副武器的唯一标识,标识的生成规则由服务器决定。

If-None-Match(带过去):请求服务器时,以If-None-Match将E-tag的内容带过去。服务器收到If-None-Match的字段之后,对服务器资源进行标识比对,如果没有匹配的话,则返回200,有匹配的话就返回304.

how-vue-work

Posted on 2019-08-28

JS中的setTimeout和promise是怎么回事?详解event loop机制

Posted on 2019-08-28

前言

在说内容之前,先发一道面试题,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//例题1
console.log('script start');

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

Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});

console.log('script end');

这个题目比较简单,应该能够看出来结果是:script start -> script end -> promise1 -> promise2 -> setTimeout

那为什么是这个顺序呢,结合一些网上的文章,我尝试用自己的理解或者说法去记录这个知识点。

浏览器内核渲染进程介绍

在浏览器的每一个Tab,都被认为是一个渲染进程,渲染进程内部分为多个线程,这多个线程之间相合作,各自管理着各自专注的领域的事情:

  1. GUI渲染线程
    • 渲染页面,计算布局和绘制样式
    • 重绘和回流时会执行
    • 和JS引擎线程互斥 (防止结果混乱不可预期)
  2. JS引擎线程
    • 解析和执行JS代码
    • 单线程
    • 和GUI渲染互斥(防止结果混乱不可预期)
  3. 事件触发线程
    • 管理事件的循环,鼠标点击/滚动、setTimeout、ajax
    • 在达到条件时,将回调方法放入JS引擎的执行队列
  4. 定时器触发线程
    • setTimeout和setInterval的管理线程
    • 定时任务由定时器触发线程计时(如果定时器不多的页面,这个线程岂不是很空闲)
    • 当计时完毕,通知事件触发线程
  5. 异步http请求线程
    • 发起和处理异步请求的线程
    • 当请求完成,有回调时,通知事件触发线程干活

Event loop机制介绍

Eventloop 介绍

首先我们要明确一个点,JS是从上而下执行的,然后遇到异步的方法的时候,再去进行一些处理,这个异步处理的机制就是要去了解的Event loop。

在JS执行过程中,浏览器分配堆内存去存储数据,而JS方法的执行上下文(调用栈)则是如同其名,是用栈结构存储的。

在浏览器JS执行的过程中,遵守一种策略:

  1. 执行栈里面的任务,遇到异步的任务,先交给对应的线程去处理
  2. 执行完栈内的任务后,询问事件触发线程,是否有新的回调(来自多处)可执行,有的话,执行栈被重新添加任务执行
  3. 重复1-2步

那什么是宏任务、什么是微任务呢?

我觉得说这两个之前,得先解释一下浏览器的JS引擎线程跟GUI线程之间的执行顺序先。上面提到这两个线程是互斥的,那么就有一个机制,让他们能够有序地执行。这个机制,就是轮流…对就是这么简单。JS引擎执行完,轮到GUI引擎执行一下,如此往复:JS -> GUI -> JS -> GUI。那么又有新的问题了,JS引擎怎么才叫执行完一次呢?其实就是上面Event loop机制提到的第一步执行完了就算一次执行完。

一个执行栈的同步执行的代码,被认为是宏任务。

eg1:

1
2
3
4
5
6
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';
// body的颜色只会变一次,因为都是同步的,同一个宏任务内执行完成。
// 去到GUI引擎那里只会认为是要把背景色变成灰色

eg2:

1
2
3
4
5
6
document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)
// body背景色先变蓝然后马上变黑
// 说明是分成2次宏任务执行,第一次变蓝,然后GUI执行了,第二次宏任务设置变黑,然后GUI再执行

那说完宏任务,什么是微任务?

微任务是在宏任务执行指挥立即执行的任务。包括Promise.then process.nextTick

微任务在宏任务和GUI之间执行。所以上面的JS -> GUI -> JS -> GUI流程可以改成:宏任务 -> 微任务 -> GUI -> 宏任务 -> 微任务 -> GUI ···

eg3:

1
2
3
4
5
6
7
8
9
document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
// 输出1 、 3、 2
// GUI : 背景直接变黑,没有变蓝

图示:

任务流程

参考文章

从多线程到Event Loop全面梳理
JS(浏览器)事件环 (宏、微任务)

JS 中的继承

Posted on 2019-08-01

在其他 OO 语言当中,接口继承和实现继承。但是 JS 只支持实现继承。JS 的继承通过原型链来实现。

JS 继承的几种方式

  1. 原型链继承
  2. 借用构造函数
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

一、原型链继承

【定义】通过将子类的构造函数原型指向父类的实例,来达到继承的目的。

【缺点】

  1. 构造函数被替换
  2. 在子类上挂载方法,要在替换完原型之后,因为整个原型换掉了
  3. 父类(父实例)上的引用类型的属性,会被子类的各种实例公用

【实例】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fucntion SuperType () {
this.property = true
}

SuperType.prototype.getSuperValue = function () {
return this.property
}

function SubType () {
this.subproperty = false
}

SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function () {
return this.subproperty
}

var instance = new SubType()
// instance 从SuperType继承了方法和属性
instance.getSuperValue() // true

二、借用构造函数继承

【定义】在子类构造函数的内部调用超类型构造函数。

【缺点】

  1. 方法都在借用构造函数内声明,不复用
  2. 方法都在超类内部,不透明

【实例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.color = ['red', 'blue', 'green']
// 【缺点】方法们必须写在构造函数里面
this.sayColor = function() {
alert(this.color)
}
}

function SubType(params) {
// 将SuperType执行一遍,东西都挂在this上面
SuperType.call(this, params)
}

var instance1 = new SubType(params)

instance1.color.push('black') // ['red', 'blue', 'green', 'black']

// 各自独立,因为各自用各自的this造的
var instance2 = new SubType(params)

instance2.color // ['red', 'blue', 'green']

三、组合继承/伪经典继承

【定义】组合继承,也叫伪经典继承。通过将借用构造函数和原型链两者的技术(优点)组合在一起,用原型链处理属性+方法,用借用构造函数的方法去实现对实例属性的继承。

【优点】

  1. 用原型链实现对原型属性和方法的继承
  2. 通过借用构造函数实现对实例属性的继承

【缺点】要执行 2 次超类的构造函数,一次是在构造函数里面,另一次是要生成被子类继承的实力的时候。

【实例】

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
function SuperType (name) {
this.name = name
this.color = ['red', 'blue', 'black'],
}
SuperType.prototype.sayName = function () {
return this.name
}

function SubType (name, age) {
// 借用构造函数,对name属性的继承独立挂载
SuperType.call(this, name)
this.age = age
}
// 指向原型,建造原型链关系
SubType.prototype = new SuperType()
// 重新指定构造函数
SubType.prototype.constructor = SubType
// 重写sayName方法
SubType.prototpye.sayAge = function () {
alert(this.age)
}

let instance1 = new SubType('Tome', 29)
instance1.color.push('yellow')
alert(instance1.color) // ['red', 'blue', 'black', 'yellow']
instance1.sayName() // Tom
instance1.sayAge() // 29

let instance2 = new SubType('Mary', 19)
instance2.color // ['red', 'blue', 'black']
instance2.sayName() // Mary
instance2.sayAge() // 19

四、原型式继承

【定义】原型可以基于已有的对象创建新的对象。创建一个空的方法,然后将要继承的实例给予空的方法,再用新的方法创建新的实例,就完成了对超类的继承。

【优点】不用创建一个超类的实例去给子类继承。

【缺点】跟原型链继承一样,实际上原型式继承就等于原型链继承的一个马甲。

1
2
3
4
5
6
function Object(o) {
// o 可以是一个实例或者一个原型,o被所有Object方法创建的饭实例所共享。
function F() {}
F.prototype = o
return new F()
}

五、寄生式继承

【定义】在一个方法内部,借用另一个方法去复制原有的对象,然后对克隆体进行赋值,最后返回克隆体。

【特点】产生的克隆体跟构造函数没什么关系,克隆体是通过内部的方法复制出来的,原型链指向的是本体的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createAnother(origin) {
var clone = object(origin)
clone.sayHi = function() {
console.log(this.name)
}
return clone
}

var person = {
name: 'tom',
age: 18
}

var anotherPerson = createAnother(person)
anotherPerson.sayHi() // tom

构造函数的发展和种类

Posted on 2019-07-26

最近在重新翻阅《Javascript 高级程序设计》,读到了构造函数的种类的发展历程,发现自己之前只是有一个模糊的认识,停留在会用,或者知道怎么用,但是不知道个中原因或者发展历程,没有形成体系化的知识结构,现在借此机会来梳理一下。

创建对象的方法(构造函数种类)

  1. 工厂模式
  2. 构造函数模式
  3. 原型模式
  4. 组合模式:构造函数+原型模式
  5. 动态原型
  6. 寄生构造函数模式
  7. 稳妥构造函数模式

一、工厂模式

工厂模式就是很简单的一个function里面生成一个对象,给对象赋予属性和方法之后return出来。

【缺点】生成的对象难以识别是什么类型。也就是说生成出来的东西不知道是什么class。

【示例】

1
2
3
4
5
6
7
8
9
10
function createPersion(name, age, gender){
let person = new Object()
person.name = name
person.age = age
person.gender = gender
person.sayHi = function(){
console.log(`Hi, my name is ${this.name}`)
}
return person
}

二、构造函数模式

为了解决工厂模式的缺点,诞生了构造函数模式。

【原理】由于在JS的世界中,任何function在new操作符操作下,都会成为构造函数,用以创建特定类型的对象。构造函数中如果没有return语句,就会将构造函数的this(运行环境)return出来,如果有return语句的话,则是以return语句返回的内容为准。

new 操作符做了什么?

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象
  3. 执行构造函数的代码
  4. return新对象

其实感觉就是将构造函数做的事情,包裹在new操作符里面做了。


【优点】

  1. 方法内部无需显式创建对象
  2. 直接将方法和属性赋值给this
  3. 没有return
  4. 可以通过实例的( constructor属性 || isntanceof方法better )来判断对象类型
    1. 通过instanceof方法,判断只要是实例中包含的继承过的类的都算true

【缺点】构造函数中的方法,会重复生成,每生成一个实例都会生成一堆对应的方法,而且互不相等。在下面的例子中,Tom和Li Lei都有sayHi方法,虽然做的都是一样的事情,但是都各自生成了一个。会造成浪费。

缺点的丑陋解决办法,将方法都挂在共享的作用域下,去读取,但是没有形成封装性。详见示例2。

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person (name, age, gender) {
this.name = name
this.age = age
this.gender = gender
this.sayHi = function(){
console.log(`Hi, my name is ${this.name}`)
}
}

let person1 = new Person('Tom', 16, 'male')
let person2 = new Person('Li Lei', 18, 'male')

console.log(person1.sayHi === persion2.sayHi) // false

// 这样执行的话Mary等属性会在window下,因为Person执行的时候this在window上
Person('mary', 26, 'female')

【示例2】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 【缺点】的丑陋解决办法
function sayHi(){
console.log(`Hi, my name is ${this.name}`)
}
function Person (name, age, gender) {
this.name = name
this.age = age
this.gender = gender

// 使用挂在全局的共享方法,但是方法一多怎么办?
this.sayHi = sayHi
this.sayHi2 = sayHi2
this.sayHi3 = sayHi3
// ....无穷无尽,代码也丑陋......
}

三、原型继承

【原理】原型继承的思想就是将属性和方法都挂载在构造函数的prototype对象上,这个对象的作用就是包含这个构造函数的类型的所有共享属性和方法。其实名字叫“原型”已经挺好理解了,就是所有实例的一个母版,原型上有的,实例都有。

了解原型继承需要对原型和原型链有所了解,这个段落不做介绍,日后写一个文章专门说明。

【优点】封装在一起,可读性比较好,方法都可以共用。能设置默认值(书中认为是一个缺点,看各人理解吧)。

【缺点】prototype上挂载的属性或者方法,是所有实例所共享的。一旦其中示例中的一个引用类型被进行了修改,那么所有其他实例中的值也会被改动到(引用类型都是指针)。

【实例】

1
2
3
4
5
6
7
8
9
10
function Person () {}
Person.prototype.name = 'Tom'
Person.prototype.age = 18
Person.prototype.gender = 'male'
Person.prototype.sayHi = function(){
console.log(this.name)
}

let person1 = new Person()
console.log(person1.name) // 'Tom'

四、组合模式:构造函数+原型模式

组合继承很好理解,就是取构造函数模式和原型模式的优点作为类的一个构造模式,由于取了两种模式的优点,所以叫组合继承。

  • 属性,由构造函数模式处理。
  • 方法,由原型模式处理。

【优点】封装化(从前端角度看)。属性独立,方法公用。

【缺点】属性和方法分开声明,不是在同一个方法包裹(从其他OO语言角度)。

【实例】

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
Person.prototype.sayHi = function(){
console.log(this.name)
}

let person1 = new Person('Tom', 16, 'male')
let person2 = new Person('Li Lei', 18, 'male')

console.log(person1.sayHi === persion2.sayHi) // true

五、动态原型

动态原型是在组合模式上进行了一点小“优化”

【优点】优化封装。

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name, age, gender) {
this.name = name
this.age = age
this.gender = gender
// 这里判断条件是一个必须会有的方法就行,不用每个都判断
if(typeof this.sayHi !== 'function') {
// 开始给原型赋值方法们
Person.prototype.sayHi = function (){
console.log(this.name)
}
}
}

六、寄生构造函数模式

《高程》提到如果上面的模式都不适用的时候,可以使用寄生构造函数模式,但是我暂时没想到什么场景下有这样的需求。

【区别】为什么要专门说一下区别,因为这个跟构造函数模式太像了。但是构造函数是直接造一个对象,且对象为this,而寄生构造模式是自己内部新建一个对象(实际上做了new关键字做的事)。外部再new它其实只是为了挂constructor在实例上面而已。

【特点】说不上优点还是缺点,只能说是特点:寄生构造生产的实例,跟这个构造函数本身是没什么关联的,除了名义上是它的实例之外,其余那些属性和方法都是自己在内部生成和挂载的,跟我们声明和调用那个构造函数(和其原型也)没什么关系。

【实例】

1
2
3
4
5
6
7
8
9
function Person (name, age, gender) {
let o = new Object()
o.name = name
o.age = age
o.gender = gender
return o // return 的是自己内部new的实例,外面new操作符给的this,没用到。
}

let person1 = new Person('Tom', 16, 'male')

七、稳妥构造函数模式

稳妥构造模式在寄生构造模式的基础上,创建实例的时候去掉了new操作符。

【特点】没有公共属性,方法内部不引用this对象。适合在安全的环境中(禁用this和new)

【实例】

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person (name, age, gender) {
var o = new Object()
o.name = name
o.age = age
o.gender = gender

o.sayHi = function () {
console.log(name) // 【特点】没有使用this引用值
}
return o
}

let person1 = Person('tom', 18, 'male') // 【特点】不使用new
12

Willong lin

14 posts
14 tags
© 2020 Willong lin
Powered by Hexo
|
Theme — NexT.Gemini v6.0.0