面向过程和面向对象
面向对象(Object-Oriented,OO)和面向过程(Procedure-Oriented,PO)是两种主要的编程范式,它们在设计和实现软件系统时有着根本的不同。以下是它们之间的一些主要区别:
封装:
- 面向对象:强调数据(属性)和操作数据的方法(行为)的封装在一个对象中。对象可以看作是一个独立的实体,具有状态和行为。
- 面向过程:数据和操作数据的函数是分开的,通常函数操作全局数据或者通过参数传递数据。
模块化:
- 面向对象:通过类和对象实现模块化,每个类定义了一组相关的属性和方法。
- 面向过程:通过函数和过程实现模块化,每个函数或过程执行特定的任务。
代码重用:
- 面向对象:通过继承和多态性实现代码的重用。子类可以继承父类的属性和方法,并且可以扩展或重写它们。
- 面向过程:代码重用通常通过函数库来实现,需要显式调用函数。
数据抽象:
- 面向对象:通过类提供数据抽象,隐藏内部实现细节,只暴露必要的接口。
- 面向过程:数据抽象不是主要关注点,通常通过函数的参数和返回值来处理数据。
维护和扩展:
- 面向对象:通常更容易维护和扩展,因为对象和类的结构有助于组织代码,并且可以通过添加新的方法或属性来扩展功能。
- 面向过程:随着系统复杂度的增加,维护和扩展可能变得更加困难,因为函数和数据之间的关系可能更加分散。
设计方法:
- 面向对象:设计时更侧重于对象之间的关系和交互,以及如何通过对象的行为来实现功能。
- 面向过程:设计时更侧重于步骤和过程,以及如何通过一系列函数调用来实现功能。
多态性:
- 面向对象:支持多态性,允许不同的对象对同一消息做出响应,这使得代码更加灵活和可扩展。
- 面向过程:不支持多态性,函数的行为通常是固定的。
并发和同步:
- 面向对象:对象可以自然地表示并发实体,因为它们封装了状态和行为,可以更容易地实现线程安全。
- 面向过程:并发和同步需要显式地通过锁、信号量等机制来管理。
每种范式都有其适用的场景,选择哪种范式取决于项目的需求、团队的熟悉程度以及预期的系统复杂度。在实际开发中,很多语言和框架支持混合使用这两种范式。
(以上答案来自AI,不用纠结,与其纠结,去学一段时间的 Java ,自然就清晰起来了)
JavaScript对象的创建
创建对象有哪几种方法?
Object.create();
var object = {};
new xxx();
思考一下,第一种和第二种有什么区别?
const obj01 = Object.create({});
const obj02 = {};
console.log(obj01);
console.log(obj02);
将上述代码放在浏览器环境下执行,在控制塔中会发现:
obj01有两个【prototype】,而obj02只有一个。所以:
const obj01 = Object.create({});
const obj02 = {};
console.log(obj01.__proto__.__proto__ === obj02.__proto__); // true
每一个对象身上都会有__proto__
属性,这是一个隐式的属性(当然,JavaScript中没有真正意义上的private),属性值是一个引用,指向对应的原型对象。
const a = {};
const b = Object.create(a);
console.log(b.__proto__ === a); // true
很明显,在这个案例中,b的原型对象是a。所谓的原型对象,实际上就是产生该对象的对象。可能有点绕,分析一下。这个例子中,b是由a派生的,所以b的__proto__
属性会指向a。
本质上这就是一个面向对象中继承的过程,b是a的子类:
const a = {
sayHello:() => {
console.log("hello");
}
};
const b = Object.create(a);
b.sayHello();
b有sayHello这个方法嘛,并没有,但是它的父亲【a】有,所以它自然能够【继承】下来这个方法。
当然,用JavaScript的话术描述就是:在调用一个方法或者使用一个属性的时候,会在对象本身上面查找,找不到会沿着原型链,一层一层的向上寻找,直到null为止。
const foo = {};
console.log(foo.__proto__.__proto__); // null
Object.create(Object.prototype) 和 Object.create(null) ?
实践出真知:
const obj01 = Object.create(Object.prototype);
const obj02 = Object.create(null);
console.log(obj01);
console.log(obj02);
Object.prototype表示一个基类对象,所有的对象都是它的子对象。由null创建出来的对象(obj02),本身null已经是原型链的顶端了,所以没有任何属性。
万恶之源 —— new xxx();
如果你是一个Java程序员,那你肯定想不通的是,我们竟然可以去【new】一个函数?没错,这就是 JavaScript 好玩的地方(好玩的damn!)
function Person(name){
this.name = name;
}
const person = new Person("kaiven");
console.log(person.name); // kaiven
天塌了,不过,我们还是仔细分析一下:
首先,我们需要明确的一点是,【prototype】属性是函数特有的。普通的对象没有的。
function Person(name){
this.name = name;
}
const person = new Person("kaiven");
console.log(person.prototype); // undefined
console.log(Person.prototype); // {}
然后,谁构造了对象,对象的__proto__
属性就指向谁,如果是通过new 的方式的话,指向对应的构造函数的【prototype】属性,即原型对象:
function Person(name){
this.name = name;
}
const person = new Person("kaiven");
console.log(person.__proto__ === Person.prototype); // true
还有好玩的,函数的原型对象上有一个【constructor】属性,指向函数本身:
function Person(name){
this.name = name;
}
const person = new Person("kaiven");
console.log(Person.prototype.constructor === Person); // true
放一张图:
好玩的事情出来了,当你尝试一直点击【prototype】属性的时候,你会发现无限嵌套。虽然我也搞不懂为什么会搞出这样的机制,但是我猜测是为了借鉴 Java 。如果你学过 Java,那么你就会发现,每一个类里面都会有一个隐式的构造器,new 创建对象时,会执行对应的构造器(构造函数)。JavaScript 的设计者可能是在【不想放弃函数作为一等公民的理念】和【贴近其他面向对象语言】之间做的一个权衡吧。(很潦草,真的)
最后,对象的身上也有一个【constructor】属性,指向对应的构造函数:
function Person(name){
this.name = name;
}
const person = new Person("kaiven");
console.log(person.constructor === Person);
好了,基本上就这些了。这里提出一个疑问:函数怎么会有属性呢?
答:在 JavaScript 中,一切皆为对象!!!(我***)
new 关键字,到底干了些什么?
- 创建了一个对象
- 该对象的
__proto__
属性,指向了构造函数的【prototype】,即原型对象 - 该对象实现了这个构造函数的方法
- 根据一些特定的情况,返回对象
- 如果构造函数没有返回值,则返回我创建的这个对象;
- 如果有返回值,且是对象,则返回该对象;
- 如果有返回值,且不是对象,则返回我创建的这个对象;
让我们来手写以new
关键字吧:
function New(Father){
if(typeof Father !== "function"){ // new 关键词后面跟的是一个函数嘛,所以非函数肯定不行
throw new Error("第一参数必须传入一个函数");
}
var obj = Object.create(Father.prototype); // obj 继承 对应构造函数的原型对象
// Array.prototype.slice.call(arguments,1) => 从下标1开始截断arguments数组
// 相当于 result = Father.apply(obj,[a,b,c]) => 构造函数(Father)被obj对象调用,并且传入相应的参数,返回函数执行后的结果
var result = Father.apply(obj,Array.prototype.slice.call(arguments,1));
// 构造函数有返回值并且返回值的类型是object且返回值不为null,输出result;否则输出obj
return result && typeof result === 'object' && result !== null ? result : obj;
}
(关于最后一点,可以自行做一下实验的)
继承
继承无非就是子类拥有父类的非私有的属性和方法,在 JavaScript 中,并不存在【私有】一说。
上文我们知道,可以通过【new 函数】来创建对象:
function Parent(name){
this.name = name;
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
}
const p = new Parent("kaiven");
const c = new Child();
在这里,继承,无非就是让对象【c】可以拥有对象【p】的属性和方法。
上文我们知道,JavaScript中,每一个对象都会有__proto__
属性,用于链接上层对象。
p的__proto__
属性指向Parent.prototype;
c的__proto__
属性指向Child.prototype;
所以,在对象【c】中想要使用对象p的属性和方法,可以这样做:
function Parent(){
this.name = "kaiven";
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){}
Child.prototype = Parent.prototype;
const c = new Child();
console.log(c.__proto__ === Parent.prototype); // true
console.log(c.name); // undefined
思考一下,这样做合理嘛?
这样做,就意味着,对象【c】是Parent的原型对象派生的?很显然,它应该是Child的原型对象派生的,而且它也获取不到【name】属性,因为【name】属性是Parent原型对象派生的对象身上的。
改进一下:
function Parent(name){
this.name = name;
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){}
Child.prototype = new Parent("kaiven");
Child.prototype.constructor = Child;
const c = new Child();
console.log(c.name); // kaiven
这样其实会出现多个Child实例引用同一个Parent对象的情况,实际上是一种不安全的行为。
江湖中还有另一种写法:
function Parent(name){
this.name = name;
}
function Child(id){
Parent.apply(this,Array.prototype.slice.call(arguments,1));
this.id = id;
}
但是,这里会有两个问题:
- 属性或者方法,想要被继承的话,只能在构造函数中定义
- 如果方法在构造函数中定义了,每次都会创建
这里贴一篇文章:
https://blog.csdn.net/yuzhochaojiwudib/article/details/142034279
不要去纠结这些东西,ES6+使用Class就好了,我不知道面试会不会考哇。。。(我猜大概率也不会,babel难道不好用嘛?)
本文的核心就是原型和原型链,其他的东西都是基于这个东西发展起来的。
2024.11.15
writeBy kaiven