面向对象的三大特点:
继承、封装、多态
继承:
- 提到继承,就要涉及到类的概念,我的理解:类就是一个或一些事物的共同属性和方法的一个抽象
- 那么继承,就是表面意思,去继承他的父类所描述的那些属性和方法,并且允许它去扩展、重写等等
封装
就是将系统模块装箱,隐藏内部实现,只暴露给外部调用接口
多态
就是多种状态,接口的多种不同的实现方式
也就是说,一个父类的方法或者接口,被多个子类继承并且重写了,这样,父类的一个方法就在子类中有了多种表现形式,就形成了多态
JS 的面向对象
概述
- 面向对象是一种思想,或者说编程模式
- 它与面向过程不同,面向过程关注的是发展的过程,而面向对象关注的是参与者是谁,如何参与的?
JS 面向对象机制的前(li)世(shi)**今(**yi)生(liu)
- 对于 JS 来说,一切皆对象。这是有一定的历史原因的–因为 JS 的作者 Brendan Eich 在当时开发 JS 时,面向对象的编程思想正泛滥
- 当时的浏览器非常初级,以至于 Brendan Eich 不想把 JS 写得那么正式/复杂,它只需要满足最简单的浏览器-用户的交互需求即可,比如–填写一个用户名
Brendan Eich 的做法 1–new 构造函数
- 他参考了一下其他语言,发现都是通过 new 类的构造函数来实现的继承,然后–他就采用了一种简化的方式:抛弃类,允许在 JS 中直接 new 构造函数得到实例对象,以此实现继承。
诶,有类我不写,就是玩~
- 但是–这种方法有一个缺点–无法共享属性和方法,或者说这种方法创建的实例对象继承的只是值,而不是地址
- 这意味着如果要继承的属性有 5 个,每个占内存 10,若需要创建 100 个实例对象,那么光是这些属性所消耗的内存就是:510100,对性能很不友好
- 除此之外,如果一个实例对象添加或修改一个属性,影响不到其他的实例对象。
为了解决这个缺点–
Brendan Eich 的做法 2–引入 prototype 属性
- 为构造函数设置一个prototype 属性,专门用来存放实例对象们能够共享的属性和方法,那些不需要共享的属性和方法,就还是放在构造函数里面。
- 这样子,实例对象的属性和方法就被分成两种,一种是不共享的,另一种是共享的的。
new 关键字完全继承,
- 它其实是一个被封装的函数,其核心机制是setPrototypeOf()和apply()
- **当 new 一个实例对象时,将会****通过调用 setPrototypeOf()“自动引用”prototype 对象的属性和方法,(获取到 prototype 对象里存放的属性和方法)**
- 然后调用 apply()方法,(获取到构造函数中存放的属性和方法)
这就是原型模式
- 用于创建实例对象,实现了高性能继承。
- **在需要继承时,允许一个对象直接克隆一个已有的原型对象,以此快速地生成和它一样的新对象实例,所有****对象实例会共享原型对象的所有属性和方法**
明白了原型模式就很容易理解原型和原型链了–
原型和原型链
原型 prototype
每个函数(构造函数)都有一个prototype属性,它本身是个引用,指向一个对象,叫做原型对象,其中包含了可以由由同一个构造函数创建的所有实例对象共享的属性和方法
可以简单理解:原型就是一个模板,**可以通过克隆它实现继承
***Tips:****一个构造函数(包括它自己),和由它创建的所有实例对象都以引用的方式,共用一个原型对象,(因为在 JS 中,函数本身是对象)*
隐式原型proto
- 每个对象都有一个proto属性,指向它的构造函数的原型对象
- 即:它的值 === 它的构造函数的
[[Prototype]]
的值
我们举个栗子,就很好理解了–
1 | let constructor = function () {}; |
一个大坑:“proto”这个属性的正确写法是两边是各两个下划线“_”
ES6 之后更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf
和Object.setPrototypeOf/Reflect.setPrototypeOf
不推荐直接使用该属性:
为什么?
- 因为proto前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API ,只是由于浏览器广泛支持,才被加入了 ES6 。
- 无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的 Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
Obj.constructor 属性
是另一个对象的属性,它指向该对象的构造函数
1 | function company() { |
原型链
每个对象都有proto属性,来指向它的构造函数的原型对象,然后原型对象也是对象,也拥有proto属性…然后就这样一直往上层指,直到null
(链的根部或者尾部就是 null),就形成了一条原型链
原型链查询
当访问一个对象的属性时,如果该对象内部没有这个属性,那么就会去它的proto属性所指向的那个原型对象里找,如果还找不到,就继续往父级的构造函数的原型对象里找…直到原型链顶端 null
这个过程和执行上下文、作用域链很像
JS 实现继承的几种方式?
1.构造函数+apply 继承
1 | function company() { |
只能继承构造函数里的属性,不能继承构造函数原型及原型链上的属性,如下 ↓
1 | function company() { |
2.prototype 原型继承
原型继承更高效且节省内存,也更 JS
1 | function company() { |
只能继承构造函数的原型对象(prototype)和原型链上的属性,不能继承构造函数内部的属性
1 | console.log(obj1.name); //undefined |
3.new 关键字类式继承
概述
- JS 中其实是没有类的概念的,所谓的类是构造函数模拟出来的。当我们使用 new 关键字的时候,或者 ES6 的 Class 关键字,就感觉“类”的概念很像 java。但其实,new 和 class 关键字底层实现机制还是基于原型的,它们都是语法糖。
那么在 JS 中,是如何实现类继承的?
- 就是构造函数当做父类,然后通过 call 和 apply 方法,改变 this 的作用环境,使得子类能够获取到父类的各种属性。可以说 ES6 引入 call 和 apply 方法一部分原因就是为了更好地面向对象。
注意:
- JS 函数中的 this 对象就像是一个函数的隐式参数,或引用
- 实际上 new 关键字–是一个封装好的函数,其内部机制是上面第 1、2 个方法的组合,因此这种方式融合了二者的优点,能完全继承构造函数内部属性+原型对象里的属性+原型链上属性
1 | function company() { |
来看一个稍复杂点的栗子
在函数对象内用过 apply 调用父类的构造函数,使得自身获得父类(父级构造函数)的方法和属性****
1 | var father = function () { |
在上面代码中 ,首先,new 关键字内部执行到– let result = child.apply(man, null);
会调用 child 函数,然后将其中的 this 直接换成 man,就像这样 ↓
1 | var child = function () { |
然后,执行到了 call 方法,同理,执行 father 方法并将其中的 this 换成 man–
1 | var father = function () { |
然后,man 对象已经存在那些属性和方法了,因此直接调用 man.say()即可
或者–灵活运用 Object.create()方法、obj.constructor 属性
Object.create()方法
- 逻辑:该方法会创建一个新对象,并使用传入的对象当做新创建的对象的proto,然后返回这个新对象
- 参数:对象
- 返回值:对象
因此,这可以是另外一种创建实例对象的方式,你可以灵活运用它
1 | function company() { |
obj.constructor 属性
而对于 constructor 属性,你可以灵活运用该方法来实现构造函数之间的继承(因为只有函数有这个属性)
1 | function Parent() { |
《JS 中 new 操作符做了什么?》–Crushdada’s shimo Notes
评论