一篇文章搞定“JavaScript面向对象”
面向对象的三大特点:
继承、封装、多态
继承:
提到继承,就要涉及到类的概念,我的理解:类就是一个或一些事物的共同属性和方法的一个抽象
那么继承,就是表面意思,去继承他的父类所描述的那些属性和方法,并且允许它去扩展、重写等等
封装
就是将系统模块装箱,隐藏内部实现,只暴露给外部调用接口
多态
就是多种状态,接口的多种不同的实现方式
也就是说,一个父类的方法或者接口,被多个子类继承并且重写了,这样,父类的一个方法就在子类中有了多种表现形式,就形成了多态
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]]的值
我们举个栗子,就很好理解了--
let constructor = function () {};
let obj = new constructor();
console.log(obj.__proto__ === constructor.prototype);
//true
一个大坑:“__proto__”这个属性的正确写法是两边是各两个下划线“_”
ES6之后更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf和Object.setPrototypeOf/Reflect.setPrototypeOf
不推荐直接使用该属性:
为什么?
因为__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API ,只是由于浏览器广泛支持,才被加入了 ES6 。
无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
Obj.constructor属性
是另一个对象的属性,它指向该对象的构造函数
function company() {
this.name = " name";
this.address = "address";
}
let obj = {};
Object.setPrototypeOf(obj, company.prototype);
console.log(obj.constructor === company); //true
// 和obj.__proto__联动一波,增进理解
console.log(obj.__proto__ === obj.constructor.prototype); //true
原型链
每个对象都有__proto__属性,来指向它的构造函数的原型对象,然后原型对象也是对象,也拥有__proto__属性...然后就这样一直往上层指,直到null(链的根部或者尾部就是null),就形成了一条原型链
原型链查询
当访问一个对象的属性时,如果该对象内部没有这个属性,那么就会去它的__proto__属性所指向的那个原型对象里找,如果还找不到,就继续往父级的构造函数的原型对象里找...直到原型链顶端null
这个过程和执行上下文、作用域链很像
JS实现继承的几种方式?
1.构造函数+apply继承
function company() {
this.name = " name";
this.address = "address";
}
let obj = {};
company.apply(obj);
console.log(obj);
//{ name: ' name', address: 'address' }
只能继承构造函数里的属性,不能继承构造函数原型及原型链上的属性,如下↓
function company() {
this.name = " name";
this.address = "address";
}
company.prototype.phone = 110;
let obj = {};
company.apply(obj);
console.log(obj.phone); //undefined
2.prototype原型继承
原型继承更高效且节省内存,也更JS
function company() {
this.name = " name";
this.address = "address";
}
company.prototype.phone = 110;
let obj1 = {};
let obj2 = {};
Object.setPrototypeOf(obj1, company.prototype);
Object.setPrototypeOf(obj2, company.prototype);
console.log(obj1.phone === obj2.phone && obj1.phone === 110);//true
只能继承构造函数的原型对象(prototype)和原型链上的属性,不能继承构造函数内部的属性
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个方法的组合,因此这种方式融合了二者的优点,能完全继承构造函数内部属性+原型对象里的属性+原型链上属性
function company() {
this.name = "Billie";
this.address = "address";
}
company.prototype.phone = 110;
let obj1 = new company();
let obj2 = new company();
console.log(obj1.phone === obj2.phone && obj1.phone === 110);
console.log(obj1.name === obj2.name && obj1.name === "Billie");
// true
来看一个稍复杂点的栗子
在函数对象内用过apply调用父类的构造函数,使得自身获得父类(父级构造函数)的方法和属性**
var father = function() {
this.age = 52;
this.say = function() {
alert('hello word');
}
}
var child = function() {
this.name = 'bill';
father.call(this);
}
var man = new child();
man.say();
在上面代码中 ,首先,new关键字内部执行到-- let result = child.apply(man, null);
会调用child函数,然后将其中的this直接换成man,就像这样↓
var child = function() {
man.name = 'bill';
father.call(man);
}
var man = new child();
man.say();
然后,执行到了call方法,同理,执行father方法并将其中的this换成man--
var father = function() {
man.age = 52;
man.say = function() {
alert('xxx');
}
}
然后,man对象已经存在那些属性和方法了,因此直接调用man.say()即可
或者--灵活运用Object.create()方法、obj.constructor 属性
Object.create()方法
逻辑:该方法会创建一个新对象,并使用传入的对象当做新创建的对象的__proto__,然后返回这个新对象
参数:对象
返回值:对象
因此,这可以是另外一种创建实例对象的方式,你可以灵活运用它
function company() {
this.name = "Billie";
this.address = "address";
}
company.prototype.phone = 110;
let obj1 = Object.create(company.prototype);
//这样就和第2种继承方法一样了
console.log(obj1.__proto__ === company.prototype);
console.log(obj1.phone);
console.log(obj1.name);
//true
//110
//undefined
obj.constructor 属性
而对于constructor 属性,你可以灵活运用该方法来实现构造函数之间的继承(因为只有函数有这个属性)
function Parent() {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child() {
Parent.call(this);
this.type = 'child5';
}
// 产生一个中间对象隔离`Child`的`prototype`属性和`Parent`的`prototype`属性引用的同一个原型。
Child.prototype = Object.create(Parent.prototype);
// 给Child的原型对象重新写一个自己的constructor。
Child.prototype.constructor = Child;