类的原型与类的继承
原型prototype
创建的每一个函数都有一个prototype(原型)属性,这个属性是一个对象。而类的构造函数也是函数,只不过它是通过 new 操作符调用的,才作为构造函数,所有它也具有原型属性。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指针,指向prototype属性所在的函数(即构造函数)。原型对象的作用是为该类的所有实例提供共享的属性和方法,而构造函数中定义的属性和方法是某个实例独有的。通常,我们组合使用构造函数模式和原型模式来创建自定义类。
关于重写整个原型对象引起的问题:
let Person = function (name,age){ this.name = name; this.age = age; }; let p = new Person("Tim",18); Person.prototype = { //重写原型对象 sayName: function (){ alert(this.name); } } p.sayName(); //报错:sayName()不是一个函数
重写原型对象之前:
重写原型对象之后:
从图中可以看到,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针_proto_。重写之后,Person类就指向新的原型对象,而实例p还是指向原来的原型对象,原来的原型对象中没有定义sayName函数。
内置指针__proto__
JavaScript在创建对象时,都会定义一个__proto__内置指针,指向创建该对象的构造函数的原型对象。
- 对于普通函数或构造函数,它们都可以认为是通过new Function()创建的函数对象,因此它们的内置__proto__指向Function的原型对象,Object根类也不例外;
- 对于原型对象,它们默认是由Object构造函数创建的实例,因此它们的__proto__指向Object的原型对象;
- 注意,Function的原型对象本质是一个函数,这有区别于其它类的原型对象,使用typeOf关键字检测会有不同的效果:
type Object.prototype; //object type Function.prototype; //function
继承关系中的原型链与组合继承
什么是原型链?通过继承关系,原型对象也可能拥有原型属性,并从中继承其原型的方法与属性,这样一层一层形成链式结构,就被称为“原型链”。
原型链是实现继承的主要方法,其基本思想是:将子类的原型等于父类的实例,此时作为父类的实例,它拥有_proto_指针指向父类的原型,那么子类创建的所有实例,都能继承父类原型中的属性和方法,从而实现继承。另外,所有函数的默认原型都是Object实例,因此父类的默认原型也会包含一个内部指针指向Object.prototype,这也是所有自定义类都会继承toString()、valueOf()等默认方法的根本原因。
但仅仅使用原型链来实现继承,也会面临着同样的“共享”问题:
- 由于子类的原型作为父类的一个实例,那么父类构造函数中的属性会被子类所有实例继承,一旦有一个实例修改了这个属性,其他实例的该属性也会被影响;
- 在创建子类的实例时,我们不能向父类的构造函数传参,因为这样会像上一点介绍的那样,影响其他实例。
因此,通常会借用构造函数来实现组合继承。
function SuperType(name){ this.name = name; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ SuperType.call(this,name); //这里使用了call()来指定父类构造函数的调用对象,这样就可以为每个子类实例提供不同的父类属性值,解决了上述问题 this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.sayAge = function(){ alert(this.age); } let instance1 = new SubType("Tom",18); instance1.sayName(); instance1.sayAge(); let instance2 = new SubType("Mike",16); instance2.sayName(); instance2.sayAge();
寄生组合式继承
观察上面的组合继承可以看到,无论什么情况都要调用两次超类构造函数(上面代码标红字段):一次是在创建子类原型时,另一次是创建子类实例时在子类构造函数内部。这样,由于子类原型是超类的实例,会将超类的一些不必要的属性继承到子类原型上,看上图可以发现,子类实例中的name属性覆盖了原型中的name属性。而寄生组合式继承,就是通过创建一个超类原型的副本,而不必调用超类构造函数来指定子类原型。本质上,就是使用寄生式继承来继承超类的原型。
- 原型式继承
该继承方法的出发点是:即使不自定义类型也能通过原型来实现对象之间的信息共享,前提是已有一个对象来作为新创建对象的基础,将已有对象作为新创建对象的原型,相当于浅复制,共享引用数据类型,而修改原始数据类型属性不会影响已有对象的对应属性。
let person = { name : name; friend : ['Mike','Van']; } //非自定义类型对象,且没有构造函数 //原型式继承 function objet(o){ function F(){} F.prototype = o; return new F(); } let another = objet(person); //let another = Object.create(person); ECMAScript5新增的Object.create()方法将原型式继承规范化,可替代上面的object()方法
console.log(another.name); // "Tom" another.name = 'Jim'; console.log(another.name); // "Jim" another.friend.push('John'); console.log(another.friend);// ['Mike','Van','John']
console.log(person.name);//"Tom" console.log(person.friend);//['Mike','Van','John']
- 寄生式继承
思路与寄生式构造函数和工厂模式类似——返回的新对象与已有对象所属类的构造函数或构造函数的原型没有联系。与原型式继承一样,新对象在已有对象的基础上创建。
※寄生构造函数模式如下:
function Person(name,age){ let o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name); }; return o; }
let person = new Person('Mike',18);
console.log(person instanceof Person); //"false"
- 寄生组合式继承
通过原型式继承的原理,将超类原型(原型属性实际上也相当于一个对象)绑定到子类原型属性中,而不再使用超类实例作为子类的原型属性。
function SuperType(name){ this.name = name; } SuperType.prototype.sayName = function(){ alert(this.name); } function inheritPrototype(subType,superType){ let prototype = Object.create(superType.prototype); prototype.constructor = subType; //将对象prototype的构造函数属性指向子类 subType.prototype = prototype; //将子类的原型指向对象prototype } function SubType(name,age){ SuperType.call(this,name); //这里使用了call()来指定父类构造函数的调用对象,这样就可以为每个子类实例提供不同的父类属性值,解决了上述问题 this.age = age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); }
私有变量与静态私有变量
任何定义在函数或块中的变量,都也可以认为是私有的,因为外部无法访问其中的变量。而利用闭包的特性,可以为某个类定义私有变量并利用特权方法获取私有变量的值,这个私有变量是每个实例所特特有的。特权方法就是可以访问私有变量的公共方法。
function Person(value,name){ let id = value; //私有变量 function privateFunction(){ //私有方法 return "我是私有方法"; } this.name = name; //普通属性 this.getId = function (){ //获取私有变量的特权方法 return id; } this.getFunction = function (){ //获取私有方法的特权方法 return privateFunction(); } } let p = new Person(1,"Mike"); console.log(p.getId()); //输出:1 console.log(p.getFunction()); //输出:我是私有方法
而静态私有变量是由所有实例共享的,因此需要用到原型模式。它的特权方法需要通过私有作用域来定义私有变量和函数,这里用到了立即调用函数。
(function (){ let count = 0; //静态私有变量,统计实例个数 Person = function (value, name) { //使用函数表达式,不声明变量类型,使Person成为全局变量 let id = value; //私有变量,每个实例所独有 count++; //每调用一次构造函数就自增1 this.name = name; this.getId = function (){ //获取私有变量的特权方法 return id; } }; Person.prototype.getNumber = function (){ //获取静态私有变量的特权方法 return count; }; })(); //立即调用函数,创建私有作用域,将作用域链绑定到Person原型中 let p = new Person(1,"Jim"); console.log(p.getId()); console.log(p.getNumber()); //1 const q = new Person(2,"TOM"); console.log(q.getNumber());//2
ES 6新增的class关键字
class仅仅是对原型对象应用的语法糖。在ES 6规范以前,普通函数与构造函数是通过是否调用new关键字来区别的,而class关键字的引入使得JavaScript在类的用法上接近像Java这些面向对象的语言。同时也引入static关键字,来定义静态属性和静态方法,这是ES5不具备的。
class SuperType{ constructor(name) { this.name = name; this.func = function () { console.log('每个实例独立的方法') } } //相当于ES6以前定义在原型对象上的方法,为每个实例共享 say(){ console.log('我叫'+this.name); } //静态属性 static count = 0; //静态方法 static f(){ console.log('我是父类') } } class SubType extends SuperType{ constructor(name,age) { super(name); this.age = age; } //也可以重写方法,相当于SubType有自己的say原型方法,不会沿着原型链找父类的say() // say(){ // console.log('我叫'+this.name,'年龄是'+this.age) // } } let p = new SuperType('p'); let sub = new SubType('张三',16); sub.say()
原型方法与静态方法的区别
对于普通函数或构造函数,它们都可以认为是通过new Function()创建的函数对象,所谓的“静态方法”实际上可以理解为添加到该对象(也即这个类)的一个属性,该属性是一个方法,通常作为一个类的工具函数。因此,静态方法只能通过 类名.静态方法 的形式调用(如:SuperType.f() √),不能在类的实例上调用(如:p.f() ×),但可以被子类继承(如:SubType.f() √)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)