JS 模拟继承

一些平时用不到但是重要的东西。记录一下。

一、继承

1、什么是继承?

继承,是面向对象软件技术中的一个概念,如果一个类别 A 继承自另一个类别 B ,就把这个 A 称为 “ B 的子类”,把 B 称为 “ A 的父类”。继承可以使子类具有父类的各种属性和方法,而不需要再次编写相同的代码。

目前的编程主要分成三个流派:面向对象编程、函数式编程、其他。其中,面向对象编程中,有很多术语比如:类、对象(实例)、方法(函数)、继承(引用)、接口、协议、多态、反射等等。函数式编程中的术语包括:算子、 lambda 、柯里化、高阶函数、纯函数、副作用等等。其实他们只是将同一个东西使用不同的方式进行实现。

JS 下有 window ,window 下有 window.Object() 、 window.Array() 、 window.Function() 函数,分别是用来创建对象、数组、函数,这三个函数分别有原型属性 Object.prototype(对象) 、 Array.prototype (对象)、 Function.prototype(对象)。此时,我们实例化一个对象 let a = new Object() ,那么 a.toString() 的时候,由于对象 a 上没有 toString() 方法,则会去 a 的原型 a.__proto__ 上去寻找 toString() 方法,此时,这个对象拥有原型上的方法,看上去是 a 继承了原型上的方法,但是根据继承的定义,即继承是发生在类与类之间的,所以此处并不存在继承的关系。a 能够访问 toString() 是因为成员属性的关系,即原型链。

那 Array.prototype 和 Object.prototype 有什么关系呢?我们都知道,不管是 Array 还是 Function ,他们的 prototype.__proto__ 都指向 Oject.prototype ,即 Array.prototype.__proto__ === Object.prototype

那在 JS 里什么叫做继承?

我们都知道,实现类的继承需要子类和父类,但是在 JS 中无类的概念,所以我们用子类函数和父类函数进行代替。根据以上例子,我们假设 Array 就是子类函数, Object 就是父类函数。那此时继承是子类构造出来的对象直接拥有父类的属性。举例来说:

1
2
3
let a = new 子类();
a 拥有父类对象的属性
子类继承了父类

我们以 Array 举例来说,let arr = new Array(),此时 arr 拥有方法 arr.push() 、 arr.pop() 等,全部继承自 Arry.prototype。 关系为:arr.__proto__ === Array.prototypeArray.prototype.__proto__ === Object.prototypeObject.prototype.__proto__ === null,当获取 arr.valueOf() 的值的时候,由于 Array 原型上没有 valueOf() 方法,所以就直接去 Object 的原型上去查找。所以在 JS 中,继承的实质就是两次的原型搜索。

2、ES5 实现

在 ES6 出现前,尽管没有类的概念,我们还是能够通过原型链实现继承的功能。看代码:

1
2
3
function Human(){
// Humane 类
}

在 ES5 中,能产生对象的东西即为类。当我们实例化一个 Human 的时候,即 var person = new Human() 时,我们在控制台打印输出 person 可见其原型是 Object.prototype 。此时,可以认为 Human 是一个类。
下面写一个 Demo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Human(name){
this.name = name;
}

Human.prototype.run = function(){
console.log('Human run')
}

function Man(name){
Human.call(this,name);
this.gender = '男';
}

Man.prototype.fight = function(){
console.log('Man fight')
}
Man.prototype.__proto__ = Human.prototype;

let person = new Man('Summer');

首先定义一个 Human 类,Human 类具有属性 name ,并且原型上具有方法 run 。 定义一个 Man 类, Man 具有属性 gender ,并且原型上具有方法 fight 。首先进行属性继承, Man 继承 Human 的 name ,只需要在 Man 中调用 Human ,并且传入 this 和 name 值即可。要继承 Human 的 run 方法,我门要做的是使 Man.prototype 的原型指向 Human 的原型,即 Man.prototype.__proto__ = Human.prototype
注意:

  • 只有构造函数有 prototype 属性,作用是存放共有属性的地址

以上有一个很重要的问题,IE 不支持 Man.prototype.__proto__ = Human.prototype( 其实不支持的是 xxx.__proto__写法 ) 的写法。解决方法如下:

1
2
3
var f = function(){}
f.prototype = Human.prototype
Man.prototype = new f();

这样一来,Man,prototype.__proto__ === f.__proto__ === f.prototype === Human.prototype。此处不可以直接使用 Man.prototype.__proto__ = new Human() 进行原型链的继承,因为 Human 中有属性 name ,而我们只想使用他的原型,所以此处引入了一个空的对象 f ,为了去掉 Human 的 name 书香,这样通过这个空的对象,我们就能使 Man.prototype.__proto__ 指向了 Human.prototype

这里讲一下当 new 一个对象的时候let obj = new Fn()都做了什么:

  • 产生一个空对象
  • this = 空对象
  • this.__proto__=Fn.prototype
  • 执行 Fn.call(this,arguments)
  • return this

3、ES6 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Human(name){
constructor(name){
this.name = name;
}
run(){
console.log('Human run')
}
}

class Man extends Human{
constructor(name){
super(name)
this.gender = '男'
}
fight(){
console.log('Man fight')
}
}

ES6 的继承,我们首先定义一个类 Human ,自身的属性我们可以写在 constructor 构造函数里面,共有属性可直接与 constructor 并列写在一起

继承类的类 Man 通过关键字 extends 实现继承 ,相当于 JS 中执行代码 Man.prototype.__proto__ = Human.prototype。在 Man 的构造函数中, super 相当于调用 Human ,相当于 ES5 中的 Human.call(this,name)

以上,虽然 Man 已经声明了是 class ,但是在使用 typeof Man 时显示 Man 的类型仍然是 function。

这种使用 class 实现继承的方法固然好,但是仍存在一些问题,比如我们要在 Human 原型中声明一个属性而不是方法的时候, class 不能为我们很好的实现。

二、mixin 、柯里化等常见属于

1、mixin

先看一下 mixin 的实现原理:

1
2
3
4
5
var mixin = function(a,b){
for(let key in b){
a[key] = b[key]
}
}

mixin 实现的是将一个对象的属性复制给另一个对象。现在前端实现这个功能使用的方法是 Object.assign(a,b),这样 b 的属性就赋给 a ,并且 a 保留了自身的属性,同时 b 的属性不会发生变化。

2、柯里化

先讲一下函数,函数是一个元素对另一个元素的映射,可以理解为 y = f(x) 。复杂一点的话,出现了 x,y) -> z ,即 z = f(x,y) 。紧接着,如果 x ,y 中的某一个值为固定为常数,即 f(x,y) 变成 f(1,y),写成 g(y),那么我们可以认为 g(y) 为偏函数。柯里化就是把关于 x 、 y 的函数中的 x (或 y) 参数固定下来,得到新的关于 y (或 x) 的函数,这个过程叫做柯里化。其中参数的数量可以为多个。

用代码举个例子:

1
2
3
4
5
6
let f = function(x,y){
return x + 2*y
}
let g = function(y){
return f(1,y)
}

以上,在调用 f(1,2)g(2)得到的结果是一样的。
再举个例子,比如说写个函数 add(1)(2) ,得到的结果是 3 :

1
2
3
4
5
let f = function(x){
return function(y){
return x+y
}
}

Demo:

1
2
3
4
5
6
7
8
9
var cache = [];
var add = function(n){
if(n===undefined){
return cache.reduce((p,n) => p+n,0)
}else{
cache.push(n)
return add
}
}

在执行 add(1)(2)(3)(4)时最后返回 10 。以上两个例子其实都是柯里化的一种,我们会在函数中传递很多个参数进去,但是会逐一固定其中的某一个值进行处理,参考最初介绍的偏函数。

3、高阶函数

先看定义:
高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

相关面试题(react):

  • 什么是高阶组件?即一个组件接受一个组件并返回一个组件就是高阶组件,详情再看下文档吧

三、Web 性能优化

1、什么是 Web 性能优化

以用户为例,如果用户觉得打开你的网站加载很快,那么可以称之为 Web 性能优化。

2、在搜索栏输入 url 按下回车后都发生了什么?

  • 缓存 : 查看是否有缓存
  • DNS 查询
  • 建立 TCP 连接(三次握手)
  • 发送 HTTP 请求 Etag -> 开始后台处理
  • 接收响应
  • 接受完成 -> HTML -> 解析 HTML
  • 根据 Doctype 进行文件解析
  • 逐行解析(阅读)
  • 看到 html 标签后,chrome 会在所有的样式下载完之后再去渲染标签,以防止 css 将标签隐藏,而 FF 的渲染机制正好相反。也就是说 css 的解析会阻塞 html 的渲染
  • 看到 CSS 进行下载,css 的下载可以同时进行,chrome 最多可同时下载 8 个 css 文件,IE 可同时下载 4 个 css 文件。下在 css 是并行的,解析 css 是串行的
  • 看 JS ,并行下载,串行解析。会阻塞 HTML 渲染

3、开始优化

根据以上的 11 个步骤,我们开始进行优化。

1) DNS 查询

DNS 查询无法优化其查询过程,但是可优化其数量,如减少 DNS 查询。比如我们请求 www.a.com/index.html ,他的 css 是在 CDN 上的,需要对地址 cdn.com/a/b.css 进行请求。为了减少 DNS 查询,可以将 css 资源放在 www.a.com 下。

2) TCP 连接

TCP 连接复用,在 HTTP 请求头中添加 keep-alive,那么服务器就会尽量去复用这个 TCP 连接,也就是说第一次建立连接之后,不断开连接,在进行第二次请求的时候直接复用之前的连接即可。

如果使用了 HTTP 2.0 版本,其连接复用率是较高的,为多路复用。

3)发送请求
  • 请求一共分为四个部分,请求行、请求头。请求头减少 cookie 体积。
  • 使用 CacheControl 减少发送请求的次数。
  • 同时发送多个请求(不需要人为操作,浏览器会自己进行并行请求多个文件)
  • 同一个域名下同时能够发送 4-8 个请求,所以可以将 css 和 js 放在不同的 CDN 下,这样并行的下载文件个数由 4-8 增加到 8-16。这样跟 DNS 优化方案看起来背道而驰。那如何权衡二者的关系呢?如果项目文件很少的情况下,我们可以将所有资源放在同一个域名下,文件较多的情况下可放在不同 CDN 下进行请求。并且 CDN 的请求是不会带上 cookie 的,所以其 cookie 的大小为 0 ,也会加速请求
4) 接收响应
  • 使用 Etag ,如果请求返回的是 304 (not modified),那么我们接收请求的体积特别小。
  • 使用 Gzip 压缩 css js img文件,然后发送到浏览器再进行解压缩
5) DOCTYPE

DOCTYPE 一定要写,否则的话浏览器需要去判断需要解析的文件为什么类型,也是会增加渲染时间或者渲染错误

6) 渲染标签

减少无用标签,影响较小

7) 合并资源

合并 css / js 资源,增大文件减少请求次数

8) 首屏加载

如果网站需要请求特别多的图片资源、样式和 js ,可以使用首屏加载,加快展示首屏的速度,在页面滚动的时候加载将要展示的资源或者使用 loading 动画以增加用户的等候时间。


综上,性能优化的方案之一是增加 CDN 来增加并行的下载数量,以达到最快下载。CSS 放在 head 中加载,JS 放在 body 闭合标签前加载

推荐看一下雅虎前端优化35条。

4、如何测试优化效果

在 chrome 浏览器控制台中,有 Audit ,点击运行,控制台会将需要优化的部分展示出来。