1。构造函数
说到原型,就不得不提到设计师。什么是构造函数?简单来说,就是一个用来创建对象的函数。在典型的OOP语言(例如Java)中存在类的概念。类是对象模板,对象是类的实例。然而,在ES6之前,JS中并没有引入类的概念。因此对象不是由类创建的,而是使用一个名为 构造函数 的特殊函数来定义对象及其特征。构造函数的第一个字母通常大写,并且只有与 new 关键字一起使用时才有意义。这是一个简单的例子
function Person(name,age){
this.name = name
this.age = age
this.say = function(){
console.log('my name is ' + this.name)
}
}
const p1 = new Person('张三',18)
p1.say() // my name is 张三
可以看到,构造函数与普通函数没有什么不同,但 new 关键字将其区分开来,并允许其创建实例。那么 new 关键字到底有什么作用呢?简单来说就是以下四点
- 1。在内存中创建一个新的空对象。
- 2。将新对象的原型对象指向构造函数的原型对象。
- 3。让 this 指向新对象并在构造函数中运行代码以向新对象添加属性和方法。
- 4。指定构造函数的返回值。如果是对象类型,则返回一个对象,否则返回您创建的对象。
new关键字的具体代码实现稍后讨论,这里不再详细介绍
2。对象原型
既然构造函数可以作为模板来创建多个实例对象,那为什么还要引入原型(原型对象)的概念呢。这里就不得不提到构造函数的问题,就是浪费的问题。如上面的示例所示,say 方法是在构造函数内声明的。如果我使用此构造函数创建多个实例对象,则意味着这些多个对象中有一个方法,并且它们都执行相同的操作。上面说过,new关键字为对象开辟了一块内存空间,这意味着这些方法虽然实现了相同的功能,但是一定不能分配内存空间,因为它们属于不同的实例。这就是构造函数中浪费内存的问题。因此就有了原型概念。
什么是原型?
JavaScript 规定每个构造函数都有一个指向另一个对象的原型属性。该对象的所有属性和方法都将由构造函数拥有。这样我们就可以直接在原型对象上定义这些不变的方法,这样构造函数创建的所有实例都可以共享这些方法。因此,原型的作用也可以用一句话来概括,即共享方法。
通过上面的示例,我们尝试将 say 方法连接到原型对象 Person
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.say = function(name){
console.log('my name is' + name)
}
const p1 = new Person('张三',18)
const p2 = new Person('李四',20)
p1.say(p1.name) //my name is 张三
p2.say(p2.name) // my name is 李四
可见Person创建的实例是可以调用say方法的,所以我们一般把对象的public方法放在prototype对象上。基于这一特点,可以推导出原型对象的一个主要应用,就是对内置对象方法的扩展。例如,向数组添加自定义偶数求和函数。 Array.prototype.xxx = function(){}
这里我不会讲太多细节。
说到这里,可能会有一个疑问。假设在 person 原型对象上定义了一个方法,那么为什么或如何访问 person 创建的实例对象呢?这里我们正在处理另一个概念,原型对象。
3。对象原型
使用构造函数创建的对象将具有 _proto_
属性,该属性指向构造函数的原型对象。这就是为什么我们使用构造函数创建的对象可以访问构造函数原型对象上的属性和方法。其实上面也说明了对象原型_proto_
和构造函数原型对象prototype是等价的。
console.log(Person.prototype === p1.__proto__) // true
前面说过,构造函数prototype指向原型对象。类似地,原型对象也有一个构造函数属性,它指向构造函数本身。 constructor属性的作用主要是记录对象引用的是哪个构造函数,并且可以使原型对象指向回原来的构造函数。有什么用呢?
通常,对象方法是在构造函数原型对象中设置的。如果有多个对象方法,我们可以以对象的形式给原型对象赋值,但是这样会覆盖构造函数原型对象原来的内容,所以修改后的原型对象的构造函数将不再指向当前构造函数并将指向 Object。 **此时我们可以在修改后的原型对象中添加一个指向原始构造函数的构造函数。下面举个例子来说明:
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype = {
constructor:Person,//如果我们以对象的形式给原型对象赋值,则必须手动的利用constructor指回原来的构造函数
say:function(){
console.log('我会唱歌');
}
}
const p1 = new Person('张三',18)
p1.say()
4。原型链
了解了原型之后,就不难理解原型链的概念了。首先我们需要考察一下js的成员搜索机制。
- 1。当访问对象的成员时,首先检查该成员是否拥有该对象本身。
- 2。如果没有,则寻找其对象原型
- 3。如果尚不存在,则查找原型对象(对象的原型对象)的原型。
- 4。依此类推,直到找到对象(空)。
__proto__
对象原型的意义是为对象成员搜索机制提供一个方向或者路径,这个路径就是一个原型字符串。
5。遗产
了解了原型和原型链的概念后,终于可以讲继承了。 ES6之前,没有类的概念,所以继承方式是组合继承方式构造函数+原型对象。
为什么会有这样的继承机制呢?正如前面提到的,我们一般在构造函数中声明对象的公共属性,在原型对象中声明对象的公共方法。这就催生了遗传的想法。我们可以使用构造函数来继承父类的属性,使用原型链来继承父类的方法。下面详细解释
1。借用构造函数继承父类的属性
这一步很容易理解。我们只能在子构造函数中重现父构造函数中的操作。但有一个问题。前面提到,构造函数中的this指向构造函数对象的一个实例。因此,我们在子类构造函数中直接调用超类构造函数是不可接受的,因为此时它还没有指向子构造函数对象的实例。 所以我们需要使用call方法来改变它。
function Father(name,age){
this.name = name;
this.age = age;
this.run = function(){
console.log('我会跑步')
}
}
function Son(name,age){
Father(name,age)
}
const p1 = new Son('小明',18)
console.log(p1.name) // undefined
可以看到子类中是不能直接调用父类的构造函数的
function Son(name,age){
Father.call(this,name,age)
}
const p1 = new Son('小明',18)
console.log(p1.name) // 小明
属性继承完成了,我们来讨论方法继承
2。借用原型对象继承父类的方法
我们已经知道公共构造函数方法都是在原型对象中定义的,所以我们可以这样做,直接让子类构造函数的prototype属性指向父类构造函数原型对象?
这样确实可以达到继承父类方法的目的,但是这样会存在隐患,因为一旦修改了子原型对象,父原型对象也会随之修改。因此,不推荐。那么什么是正确的做法呢?
我们已经知道原型对象的方法是所有实例对象共享的。因此,父类的构造函数创建的实例对象必须在其对象原型中具有的所有方法。因此,这个实例对象可以作为子类构造函数的原型对象,并生成一个原型字符串。同时也实现了方法继承。
同时,因为子类的原型对象等于父类的实例,而父类的实例创建后,又开辟了额外的空间,所以父类原来的原型对象不会受到影响。这里需要注意的是,因为我们修改了子类构造函数的原型对象,所以我们需要使用构造函数来指向原来的子类构造函数。
具体实现如下
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.say = function() { // 这是子类的独有方法
console.log('我是小孩');
}
const p1 = new Son('李四',10)
p1.run() // 我会跑步
p1.say() // 我是小孩
至此,父类所有成员的继承就实现了。可见,实现继承本质上是在原型链上添加了一个端点。事实上,JavaScript 中最常见的继承类型是组合继承。
但是,通过观察组合继承的实现过程可以看出,无论什么情况,组合继承都会调用两次构造函数:一次在子类构造函数内部调用,另一次在子类创建时调用。原型 当调用时。事实上,不需要单独调用父类构造函数来指定子类原型。我们可以使用 Object.create 方法创建父类原型的副本,并将其用作子类原型。
let protoType = Object.create(Father);
Son.protoType = protoType;
protoType.constructor = Son;
上述的继承方式称为寄生组合继承。父类的构造函数只被调用一次,其余操作与组合继承一致,实现效果也与组合继承相同。 ES6类继承(扩展)实际上是基于寄生组合继承来实现的。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。