前言
我们都知道OOP(面向对象编程)是一个大型语言中非常重要的一个特性,这篇文章就来给大家详细介绍JS在早期ES6之前是如何实现OOP的!
OOP是什么
OOP就是面向对象编程,它有三个特征:
- 封装 Encapsulation
- 继承 Inheritance
- 多态 Polymorphism
它的核心思想就是通过类来抽象事物,将属性和方法封装在一起,类通过继承实现代码复用,并且可以通过多态允许不同类对同一方法有同一实现,我们可以创建该类的一个具体对象,模拟现实世界的具体事物。
其它语言是如何实现OOP的?
这里拿大家熟悉的Java来举例,在Java中,有关键词Class来帮我们设计一个类,然后可以通过new 类名()的方式来创建一个具体的对象
// 定义Dog 类
public class Dog {
//属性
String name;
int age;
//构造方法
public Dog(String name,int age) {
this.name = name;
this.age = age;
}
//普通方法
public void say( ) {
System.out.println("汪汪汪");
}
}
// 通过new 来创建一个对象
myWhiteDog = new Dog("White",2);
JS的历史:为什么曾经没有class?
但在我们今天要介绍ES之前的JS,所以彼时并没有Class关键字 支持我们方便快捷地申明类和创建对象。
当初JS的发明者布兰登·艾克在1995年被网景公司催促,要求在10天内开发出JS,目的是为了实现页面的显示和交互,以增强网页的能力,这哥们考虑要实现class的设计往往需要花费大量的时间,而且老板的要求是为了注重于能够增强页面的显示,于是,他就没有像其它大型语言一样去设计class,而是用原型的一系列设计(下文我们会提到)来暂时实现面向对象的效果。
所以这就是为什么ES6之前并没有class的原因了。那么这个时候我们到底具体是用什么来实现面向对象编程的呢?且听我娓娓道来!
函数模拟类
试想一下,如果我们要创建很多有相同属性的对象,我们可以通过对象字面量的方式创建。
const zs = {
name: "张三",
age: 18,
}
const ww = {
name: "王五",
age: 20,
}
const zl = {
name: "赵六",
age: 19,
}
但这样子是不是太麻烦了?我们在做很多重复的工作欸。
于是JS的发明团队看不下去了,故使用函数来模拟一个类,该函数比较特殊,称为构造函数。
我们可以通过new这个构造函数的方式来进行对象的创建。
function Person(name,age){
this.name = name;
this.age = age;
}
let zs = new Person("张三",18);
let ww = new Person("王五",20);
let zl = new Person("赵六",19);
我们这里的函数Person在JS中,即是构造函数,也是Person类,对“人”进行了封装,申明了name和age这两个属性,所以我们可以基于这个类来创建不同的具体对象。(这也许在你看来很离谱,但当时JS就是那么窘迫,使用一个函数来身兼两职,又要当类,又要当类的构造函数,没办法,谁叫这是在10天内开发出来的语言呢)
注意:我们一般在写构造函数的时候,要将构造函数的名字进行首字母大写(Person,Dog,Apple),这是一个规范,表示一个类!
原型式的面向对象
刚刚我们已经成功创建了构造函数,并且使用该构造函数创建了对象。
那么,接下来我们该如何声明该构造函数的方法呢?
你可能会说:在构造函数中直接声明不就好啦,但这是不对的,你说的这个是JS中的闭包(函数内声明函数)。而下面才是我们的正确做法!
Person.prototype = {
sayHello: function(){
console.log("hello"+ this.name)
}
}
接下来让我向你隆重的介绍JS中的原型!!!
prototype与__proto__
在 JavaScript 中,prototype 和 __proto__ 是理解原型继承(Prototype Inheritance)的关键概念,它们密切相关但用途不同。以下是对他们两个的解释与对比:
prototype(显式原型)
prototype仅存在于函数对象上(一般是指构造函数)
它的作用是:定义由该构造函数创建的对象的共享属性和方法! 请看下面的代码
function Dog(name) {
this.name = name;
}
// 通过 prototype 添加共享方法
Dog.prototype.bark = function() {
console.log(this.name + " says: Woof!");
};
const dog1 = new Dog("Buddy");
dog1.bark(); // "Buddy says: Woof!"
__proto__(隐式原型)
JS中的每个对象都有__proto__属性,包括函数对象。
__proto__的作用是访问其构造函数的原型(prototype)。
在上面的代码中,dog1对象的__porto__就是指向其构造函数的原型prototype。
console.log(dog1.__proto__) //{ bark: [Function (anonymous)] } 表示匿名函数(anonymous)bark
能够输出{ bark: [Function (anonymous)] } 是因为我们定义了Dog构造函数的prototype为Dog.prototype.bark = function() { console.log(this.name + " says: Woof!"); };
倘若我们不定义构造函数Dog的原型prototype的话,那么则会输出{}
function Dog(name) {
this.name = name;
}
// 没有定义构造函数Dog的prototype
const dog1 = new Dog("Buddy");
console.log(dog1.__proto__); {}
这是因为每个函数都自带一个prototype属性,这个属性的初始值是一个空对象。
这里我们没定义,所以就会输出构造函数Dog自带的{}啦!
所以我们既可以定义构造函数的Prototype,也可以默认不定义,此时__proto__的输出结果也会不同!
默认__proto__
我们创建的每个普通对象的原型在默认情况下为[Object: null prototype] {}。你也可以去修改__proto__来修改它的原型。
let obj1 = {
a:1
}
let obj2 = {
b:2
}
console.log(obj1.__proto__) //[Object: null prototype] {}
console.log(obj2.__proto__) //[Object: null prototype] {}
//也可以自己修改
obj1.__proto__ = {name:"haahahahahah"}
console.log(obj1.__proto__) //{ name: 'haahahahahah' }
而我们创建的每个函数的原型在默认情况下为[Function (anonymous)] Object。同样的,你可以通过修改该函数的__proto__来修改其原型。
var fn1 = function()
{
console.log("Im a function")
}
function fn2(name)
{
this.name=name
}
console.log(fn1.__proto__) //[Function (anonymous)] Object
console.log(fn2.__proto__) //[Function (anonymous)] Object
//也可以自己修改
fn1.__proto__ = ["heheheeheheheheheheheh"];
console.log(fn1.__proto__) //[ 'heheheeheheheheheheheh' ]
这里要注意,函数的默认原型为[Function (anonymous)] Object,函数的默认原型的原型仍然是[Object: null prototype] {}
console.log(fn1.__proto__.__proto__) //[Object: null prototype] {}
原型链查找机制
那么知道了__proto__和prototype之后,原型链是如何查找的呢,请看下面的代码:
function Dog(name) {
this.name = name;
}
// 通过 prototype 添加共享方法
Dog.prototype.bark = function() {
console.log(this.name + " says: Woof!");
};
const dog1 = new Dog("Buddy");
//原型链: dog1 实例 -> Dog.prototype -> Object.prototype -> null
console.log(dog1.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Object.prototype); //true
console.log(Object.prototype.__proto__ === null) //true
在原型链的查找中,会一层一层往上找,到null则为终点,实际上万物的原型链的终点都为null。
所以
当我们访问一个对象的属性时,JavaScript 会执行以下步骤:
- 首先在对象自身属性中查找
- 如果找不到,则在其原型对象(
__proto__)中查找 - 继续沿着原型链向上查找,直到找到该属性或到达原型链末端(null)
JS的设计哲学:对象和构造函数没有血缘关系
理解了上面原型后,我希望你明白这一点:对象和构造函数没有血缘关系。
在其它传统的面向对象语言中,例如Java是有严格的继承体系,实例对象内会存放父类的信息,而且对象和类之间是严格且不可变的,所以它们是有血缘关系的。
而在JS中的原型式的面向对象中,对象实例仅仅只是通过__proto__与构造函数的prototype关联,实例仅仅只是通过new引用了构造函数这一种创建方式。它们并没有直接的关系,我的理解是构造函数只是一个工具,对象只是借用了这个工具完成了自己的产品。
下面是一些代码上的体现:
// 1. 构造函数销毁不影响实例
function Cat() {}
const cat = new Cat();
Cat = null; // 销毁构造函数
cat.__proto__ === Cat.prototype // false(但实例仍正常工作)
// 2. 动态修改原型链
function Animal() {}
const obj = {};
Object.setPrototypeOf(obj, Animal.prototype);
console.log(obj instanceof Animal); // true(临时建立"继承"关系)
// 3. 构造函数不是实例的owner
function Car() {}
const car = new Car();
console.log(car.parent === Car); // undefined(不存在血缘标记)
"没有血缘关系"准确描述了 JavaScript 原型继承的松散耦合特性。对象与构造函数之间是通过原型链建立的动态委托关系,而非传统面向对象中严格的父子继承关系。这种设计提供了更大的灵活性。
总结
本文介绍了 JS在ES6之前是如何通过原型的方式来实现面向对象的编程的,这是一种很独特的方式,也是学习JS的学者必须熟悉的一种方式,即使后面ES6引入了class语法,但其实底层仍然是通过这种方式来实现的;同时我们还了解到了这种原型式的面向对象中,对象和构造函数没有血缘关系,对象与构造函数之间是通过原型链建立的动态委托关系。这种设计提供了更大的灵活性,但也需要开发者明确理解原型机制。
🌇结尾
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。