原型与原型链 JavaScript 是一门基于原型的语言,在软件设计模式中,有一种模式叫做原型模式, JavaScript 正是利用这种模式而被创建出来原型模式是用于创建重复的对象,同时又能保证性能,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。
构造函数
提原型和原型链,就不得不提到构造函数了,因为JS中没有类(Class)这个概念,所以JS的设计者使用了 构造函数 来实现继承机制。JavaScript语言使用构造函数(constructor)作为对象的模板。 所谓“构造函数”,就是专门用来生成“对象”的函数。 它提供模板,描述对象的基本结构。
ES6中的
class
可以看作只是一个语法糖,它的绝大部分的功能,ES5都可以做到,新的class
写法只是让原型的写法更加的清晰、更像面向对象编程的语法而已。(摘自阮一峰的ES6入门)
//构造函数
function Person(uname, age , sex){
this.uname = uname;
this.age = age;
this.gender = gender ;
}
//实例化对象
let p = new Person("11",1,"1");
如上述代码所示,JS通过构造函数来生成实例。但又出现一个新的问题,在构造函数中通过this赋值的属性或者方法,是每个实例的实例属性以及实例方法,同时公共属性和方法也被创建,这样就会重复的创造无用数据,产生数据冗余。所以又设计出了一个原型对象,来存储这个构造函数的公共属性以及方法。
构造函数创建实例化的过程
- 创建一个对象
- 将构造函数的作用域赋值给对象(这样this就指向了对象)
- 执行构造函数中的代码(为对象添加实例属性和实例方法)
- 返回新对象
原型
JavaScript是基于原型的我们创建的每个函数都有一个
prototype(原型)
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
就是当我们创建一个构造函数的时候,系统就会自动分配一个 prototype
属性,而这个prototype属性对应的就是原型对象,可以用来存储可以让所有实例共享的属性和方法。
原型对象有一个constructor属性,对应其构造函数。
并且每一个对象都有一个__proto__属性去对应他的原型对象。
用一张图来解释
原型特点
function Father(name) {
// this 指向创建它的实例对象
this.name = name;
this.age = age;
this.sex = sex;
}
Father.prototype.money = function() {
console.log(this.name+"----赚钱");
}
let p1 = new Father("zhangsan");
let p2 = new Father("lisi");
p1.money();//zhangsan----赚钱
p2.money();//lisi----赚钱
从这段代码我们不难看出:
- 实例可以共享原型上面的属性和方法
- 实例自身的属性会屏蔽原型上面的同名属性,实例上面没有的属性会去原型上面找
原型链
既然原型也是对象,那么他有自己的原型对象么,答案是肯定有的。原型的原型也有自己的原型不过这时这个原型为null
JavaScript
中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链
同样的我们用一张图解释
- 所有原型链的终点都是
Object
函数的prototype
属性 Objec.prototype
指向的原型对象同样拥有原型,不过它的原型是null
,而null
则没有原型
清楚了原型链的概念,我们就能更清楚地知道属性的查找规则,比如前面的 person
实例属性.如果自身和原型链上都不存在这个属性,那么属性最终的值就是 undefined
,如果是方法就会抛出错误
鉴于原型的特点和存在的问题,所以我们在实际开发中一般不会单独使用原型链。一般会使用构造函数和原型相配合的模式,当然这也就牵扯出了
JavaScript
中另一个有意思的领域:继承
继承
我们创造两个构造函数,Father()作为父类,Son()作为子类。
我们可以用call()方法直接调用Father方法,然后this指向调用Son的对象,这样就完成了属性的继承
// 利用原型及原型链 实现继承【面向对象】
//父 构造函数
function Father(name,age,sex) {
// this 指向创建它的实例对象
this.name = name;
this.age = age;
this.sex = sex;
}
Father.prototype.money = function() {
console.log(this.name+"----赚钱");
}
// 子 构造函数
function Son(name,age,sex,email) {
// this 指向创建它的实例对象
// this.name = name;
// this.age = age;
// this.sex = sex;
// 过于繁琐,Father中有其属性的创建
// 所以直接继承Father
// 继承 属性
// 利用call() 立即调用父类方法,并传递参数
Father.call(this,name,age,sex);
// 子类自身的属性
this.email = email;
}
但是继承中难点是父类的公共方法继承
// 继承 方法
// 直接将Father的原型 赋值 给Son
Son.prototype = Father.prototype
首先我们将子类的原型直接指向父类的原型,这样我们就可以实现父类的公共方法继承
就是将两个构造函数的原型合并为一个,此时出现了新的问题:此时给Son的原型对象添加方法,Father也可以调用
所以说这种方式不可行
那么我们用原型链的处理方式将Father的实例化对象给Son作为原型
// 将Father的实例对象 赋值给Son的原型对象
Son.prototype = new Father();
// 此时给Son添加的方法 ,Father不能调用
此时给Son添加的方法,Father就不能调用了,同时我们也可以调用Father中的公共方法
console.log(Son.prototype.constructor); //结果指向Father
但是出现了一个新的问题,就是Son的原型对象的构造函数指向的是Father
所以应该修改原型的构造函数指向
// 所以将Son的原型对象的构造函数重新指向Son
Son.prototype.constructor = Son;
console.log(Son.prototype.constructor); //结果指向Son
Son.prototype.game = function () {
console.log(this.name+"----游戏");
}
let f = new Father("xx",50,"1")
let s = new Son("oo",0,"0","0202020@163.com")
f.money();
s.money();
s.game();
// f.game(); //不可用
对此,我们看到了给Son添加的方法,Father就不能调用了,同时我们也可以调用Father中的公共方法,此时的情况如下所示
利用原型的特点和存在的问题,使用构造函数和原型相配合的模式,实现了继承,现在的Son和Father就是继承的关系。emm,也是最简单的继承关系。
如果有更精辟的见解欢迎评论留言探讨,一起探讨,一起进步!