JS梳理篇:原型、原型链、继承之我的理解
如果觉得****的排版不好看,那么可以到我的 GitHub上,详情戳
1、篇幅有些长,都是自己慢慢摸索看了很多文章整理出来的。首先上代码,来引出原型的问题
// 人的构造函数
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = function () {
console.log('hello ' + this.name)
}
}
// 实例化人的对象,并初始化
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题。因为每一次生成一个实例,都为重复的内容,会多占用一些内存,如果实例对象很多,会造成极大的内存浪费。new的过程就是生成实例的过程。来面来理解下new的时候,系统做了什么事? (回头学其他面向对象的语言也是这么几件事 )
- 1. 在内存中申请一块空闲的空间,存储创建的新的对象 (不能说开辟)
- 2. 把 this 设置为当前的对象
- 3. 设置对象的属性和方法的值
- 4. 把 this 这个对象返回
下面的输出结果为证:不同一块区域空间,如果多了那么自然占用内存l
console.log(p1.sayHello === p2.sayHello) // => false
回到问题所在,重复的内容太多了。那么对于这种问题我们可以把需要共享的定义到构造函数外部,以sayHello为例:
function sayHello = function () {
console.log('hello ' + this.name)
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = sayHello
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突(全局变量泛滥)的问题。 你肯定想到了可以把多个函数放到一个对象中用来避免这个问题:
var fns = {
sayHello: function () {
console.log('hello ' + this.name)
},
sayAge: function () {
console.log(this.age)
}
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = fns.sayHello
this.sayAge = fns.sayAge
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。 但是代码看起来还是那么的格格不入,那有没有更好的方式呢? 通过原型来添加方法,解决数据共享,节省内存空间。
2、原型引入 不知有没有发现 console.log
的时候发现 prototype
或者 __proto__
的东西,以代码为例:
var arr = new Array();
console.log(arr);
对象的方法为什么不会直接在对象里面,而是在原型里面,为什么要这样做?你想想在对象上会出现什么样的事情?你创建了一个对象 然后对象存量一堆的方法,那这样好不好?再来一个对象存了一堆的方法, 数组的方法都是一样的,所以内存中有必要存一堆么?没必要吧?所以我们怎么给对象加一个方法?或者我们怎么数组加一个方法?注意,不是给某一个数组对象加方法,因为你这样加的话只有那个数组对象才有这个方法(这也就是相当于改变源码 )。给他的原型加个方法就行,这样的话你创建出来的数组对象都有这个方法。
所以更好的解决方案: prototype
Javascript
规定,每一个构造函数都有一个 prototype
属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的实例继承。通过原型来添加方法,解决数据共享,节省内存空间,这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type
属性和 sayName()
方法, 其实都是同一个内存地址,指向 prototype
对象,因此就提高了运行效率。 那么,构造函数、原型、实例对象之间有什么关系呢?
- 任何函数都具有一个
prototype
属性,该属性是一个对象 - 构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数 - 通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
- 所有实例都直接或间接继承了原型对象的成员 ()
3、原型链:属性成员的搜索原则
3.1 原型链:是一种关系,实例对象和原型对象之间的关系,关系是通过原型( __proto__
)来联系,因为实例对象中的__proto__
原型指向的是构造函数中的原型 prototype
。
为什么实例对象可以访问原型对象中的成员。实例对象中根本没有sayName
方法,但是能够使用,这是为什么? 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性 。
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象(一直到原型链的末端 )中找到了这个属性,则返回该属性的值 ,找不到则返回
undefined
也就是说,在我们调用 person1.sayName()
的时候,会先后执行两次搜索:
- 首先,解析器会问:“实例 p1 有 sayName 属性吗?”答:“没有。
- ”然后,它继续搜索,再问:“ p1 的原型有 sayName 属性吗?”答:“有。
- ”于是,它就读取那个保存在原型对象中的函数。
- 当我们调用 p2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
3.2 如果想改变原型对象的属性值怎么办 (这其实是无意义的) ,直接通过原型对象.prototype
.属性 = 值; 可以改变。
3.3 更简单的原型语法,我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。那么代码就会显得比较臃肿,并且增加了代码开销。为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
var stu = new Person();
console.log(stu.constructor === Person) // false
console.dir(Person) // prototype: {type: "human", sayHello: ƒ}
在该示例中,我们将 Person.prototype
重置到了一个新的对象。 这样做的好处就是为 Person.prototype
添加成员简单了,这样的写法更符合开发人员的写法,不管从代码的美观程度还是对代码的实用程度来说都是如此。但是对象字面量来重写整个原型对象这样的方法:会带来一个问题,那就是原型对象丢失了 constructor
成员。也就是相当于缺少 constructor
。。
怎么回事?Person
不是实例对象 stu
的构造函数吗?当然是!只不过构造函数 Person
的原型被开发者重写了,这种方式将原有的 prototype
对象用一个对象的字面量 {} 来代替。而新建的对象 {} 只是 Object
的一个实例,系统(或者说浏览器)在解析的时候并不会在 {}
上自动添加一个 constructor
属性,因为这是 function
创建时的专属操作,仅当你声明函数的时候解析器才会做此动作。然而你会发现 constructor
并不是不存在的,下面代码可以测试它的存在性:
console.log(typeof stu.constructor == 'undefined') // false
既然存在,那这个 constructor
是从哪儿冒出来的呢?我们要回头分析这个对象字面量 {}
。因为 {}是创建对象的一种简写,所以 {}
相当于是 new Object()
。那既然 {}
是 Object
的实例,自然而然他获得一个指向构造函数 Object()
的 prototype
属性的一个引用 __proto__
,又因为 Object.prototype
上有一个指向 Object
本身的 constructor
属性。所以可以看出这个constructor
其实就是 Object.prototype
的 constructor
, 下面代码可以验证其结论:
console.log(stu.constructor === Object.prototype.constructor);// true
console.log(stu.constructor === Object);// also output true
那么有没什么办法解决这个问题呢? 所以,我们为了保持 constructor
的指向正确,建议的写法是手动恢复他的 constructor
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person, // => 手动将 constructor 指向正确的构造函数
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
}
}
console.dir(Person) // prototype: {constructor: ƒ, type: "human", sayHello: ƒ}
还有一种理解方式是:原型是对象,对象字面量创建的也是对象, 当我们在给 xxx.prototype
赋以新值的时候, javascript
将不再为该对象创建 constructor
属性。这表示对 constructor
属性的引发解释器沿着原型链查找,直至找到相应的值。
4、原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 对象的方式写原型,因为当我们在给原型增加多个属性的时候,如果重复输入xxx.prototype.xxx很多遍,那么代码就会显得比较臃肿,并且增加了代码开销,我们可以对原型使用字面量的方法来改善
- 对象的方式写原型重置了
prototype
记得修正constructor
的指向
5、继承
面向对象编程思想:根据需求,分析对象,找到对象有什么特征和行为,通过代码的方式来实现需求,要想实现这个需求,就要创建对象,要想创建对象,就应该显示有构造函数,然后通过构造函数来创建对象,通过对象调用属性和方法来实现相应的功能及需求,即可。
首先JS不是一门面向对象的语言,JS是一门基于对象的语言,那么为什么学习js还要学习面向对象,因为面向对象的思想适合于人的想法,编程起来会更加的方便,及后期的维护………… 面向对象的特性:封装、继承、多态
5.1 什么是继承
继承: 首先继承是一种关系,类与类(class与class)之间的关系,JS中没有类,但是可以通过构造函数模拟类,然后通过原型来实现继承。继承也是为了数据共享,js中的继承也是为了实现数据共享、节省内存空间。 更实际一点的意义在于:一个子类对象可以获得其父类的所有属性和方法,称之为继承。
5.2 继承的几种方法
- 原型继承 (改变原型的指向)
- 借用构造函数 (主要解决属性的问题,父级类别中的方法不能继承 )
- 组合继承:原型继承 + 借用构造函数继承 (既能解决属性问题,又能解决方法问题)
- 拷贝继承 (就是把对象中需要共享的属性或者方法,直接遍历的方式复制到另一个对象中)
- 浅拷贝
- 深拷贝
5.2.1 原型继承
function Person (name, age) {
this.type = 'human'
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
console.log('hello ' + this.name)
}
function Student (name, age) {
Person.call(this, name, age)
}
// 利用原型的特性实现继承
Student.prototype = new Person()
var s1 = Student('张三', 18)
console.log(s1.type) // => human
s1.sayName() // => hello 张三
5.2.2 借用构造函数
function Person (name, age) {
this.type = 'human'
this.name = name
this.age = age
}
function Student (name, age ,score) {
// 借用构造函数继承属性成员 这里的this student的实例对象 等于把实例对象传进去
Person.call(this, name, age)
this.score = score
}
var s1 = Student('张三', 18 ,'100')
console.log(s1.type, s1.name, s1.age) // => human 张三 18
5.2.3 组合继承
// 人的构造函数
function Person(name,age,sex) {
this.name=name;
this.age=age;
this.sex=sex;
}
// 人的原型方法
Person.prototype.sayHi=function () {
console.log("阿涅哈斯诶呦");
};
// 学生的构造函数
function Student(name,age,sex,score) {
//借用构造函数:属性值重复的问题
Person.call(this,name,age,sex);
this.score=score;
}
//改变原型指向----继承
Student.prototype=new Person(); // 继承父类不传值 指向改变 指向的是Person的实例对象
Student.prototype.eat=function () {
console.log("吃东西");
};
var stu=new Student("小黑",20,"男","100分");
console.log(stu.name,stu.age,stu.sex,stu.score); // 小黑 20 男 100分
stu.sayHi(); // 阿涅哈斯诶呦
stu.eat(); // 吃东西
最后,再次强调:面向对象特性:封装、继承、多。js不是面向对象的语言,但是可以模拟面向对象、模拟继承 为了节省内存空间