面向对象之前要知道什么是对象
面向对象(Object-Oriented Programming, OOP)是一种编程范式,它将软件设计和开发集中在对象的创建和操作上。面向对象编程旨在通过对象这一抽象实体来模拟现实世界中的事物,并通过对象之间的交互来实现复杂的系统功能。对象的三个特性(唯一标识性、状态、行为)帮助我们更好地管理和组织代码,从而提升代码的可读性、可维护性和可扩展性。
-
唯一标识性
- 定义:每个对象都有其独特的身份,即使两个对象拥有完全相同的属性和方法,它们依然是独立的两个对象。
- 意义:这强调了对象的个体性和不可替代性。在程序设计中,这意味着每个对象都有其独特的内存地址或标识符。
-
状态
- 定义:对象有状态,这意味着同一个对象在不同时间点可能处于不同的状态。
- 意义:对象的状态是通过其属性来体现的,而属性的值会变化。这种状态变化在程序运行期间对对象行为与功能有直接影响。
-
行为
- 定义:对象具有行为,它的状态可能因为其行为而改变。
- 意义:对象的行为是由方法实现的,通过调用这些方法,能够对对象的属性(状态)进行修改或者执行操作。
js
var a1 = {o:1}
var a2 = {o:2}
console.log(a1 === a2) // false 我们都知道对象本质是内存地址的指向,因此a1和a2看起来像本质也是不同的
// '行为'就是'方法','状态'就是'属性'
var o = {
d: 1, // 我是属性
f() { // 我也是属性
console.log(this.d);
}
};
面向对象编程的核心概念
-
对象(Object):对象是类的实例,集合了数据(属性)和行为(方法)。它们在运行时可以动态创建和销毁。
-
类(Class):类是对象的蓝图或模板,定义了一类对象具有的属性和方法。通过类可以创建多个具有相似特征的对象。
-
封装(Encapsulation):封装是将对象的属性和方法隐藏起来,只暴露必要的接口。这样可以保护对象的数据不被外部随意修改。
-
继承(Inheritance):继承是通过扩展已有类来创建新类的一种机制。新类称之为子类或派生类,它可以继承父类(基类)的方法和属性,并可以增加新的方法和属性或重写父类的方法。
-
多态(Polymorphism):多态是指同一个方法在不同对象中有不同的实现。多态使得可以用统一的接口来调用不同对象的方法,提高了代码的灵活性和可扩展性。
-
抽象(Abstraction):抽象是一种建模技术,关注对象的高层特征,而忽略具体的实现细节。它帮助简化复杂系统,使开发者可以专注于更高层次的问题。
面向对象编程的常用语言有Java、C++、Python、C#等。使用面向对象编程的主要优点包括:
- 代码复用性高:通过继承和组合可以重用代码。
- 可维护性强:代码的模块化和封装性提高了系统的可维护性。
- 易于理解和组织:符合人类对现实世界的思考方式,便于理解和组织复杂系统。
面向对象两大类实现方式
在开始讨论面向对象编程(OOP)之前,先简要回顾一下编程语言的两大类实现方式:基于类和基于原型。这两种方式在实现OOP时虽然途径不同,但都是为了达成相同的目标。
1. 基于类(Class-based)
大部分开发者可能都是从学习基于类的编程语言开始,比如C++和Java。基于类的语言中,编程流程通常如下:
- 定义类:先定义一个类,类包含属性(变量)和方法(函数)。
- 实例化对象:通过类创建对象,称为类的实例。
这种方式促使开发者形成一种思维定式,认为所有编程语言都需要通过定义类来实现面向对象。这类语言强调分类及类之间的关系,常见的关系如继承和组合。类通常与语言的类型系统整合,提供了编译时的类型检查和多态性等能力。
2. 基于原型(Prototype-based)
JavaScript是基于原型设计的语言。在原型机制的编程语言中,有两种主要方式:
- 引用原型:新对象持有一个原型对象的引用,共享其属性和方法。
- 复制对象:直接复制原型对象,生成新的独立对象。
总结
根据以上描述,我们可以将编程语言分为两种类型:
-
基于类的系统:
- 强调分类和类之间的关系
- 对象通过类实例化
- 类与类之间形成继承、组合等复杂关系
- 类与类型系统整合,提供编译时能力
-
基于原型的系统:
- 没有显式定义的类,通过对象实例创建新对象
- 属性和方法可直接添加到对象实例中
- 允许更高的灵活性,如随时添加新属性
这两种方式的差异主要在于:基于类的系统先定类后实例化对象,而基于原型的系统直接从对象创建新对象,无需类的定义。虽然JavaScript是基于原型的语言,但由于其在设计早期模仿了Java,引入了许多基于类的语言特性,如new
和this
。这使得开发者在实际使用中经常忽略了JavaScript原型链的独特优势。
需要注意的是,像Java这种基于类的语言,往往不允许随意向对象添加属性。而JavaScript则提供了这种灵活性,允许开发者在运行时随时为对象添加新属性或方法。这种灵活性也是JavaScript的一个显著优势。
无论是基于类还是基于原型,最终目标都是实现面向对象编程。两者只是实现方式不同,但本质都是通过对象进行封装和操作,旨在提高代码的复用性、可维护性和可理解性。因此,在选择编程语言和方法时,应根据具体需求和背景考虑,以便充分发挥各自的优势。
前端怎么创建对象
在JavaScript中,有多种创建对象的方式,每种方式都有其优缺点。主要包括字面量创建对象、工厂函数、以及构造函数。以下是对这几种方式的总结:
1. 字面量创建对象
利用字面量形式,手动创建对象。
javascript
const obj = {
name: 'Alice',
age: 25
};
优点:
- 简洁直接
弊端:
- 当需要创建多个相同结构的对象时,需要编写重复的代码。
2. 工厂函数
通过封装一个函数,批量创建对象。
javascript
function createPerson(name, age) {
const person = {};
person.name = name;
person.age = age;
return person;
}
const p1 = createPerson('w', 15);
const p2 = createPerson('ww', 155);
优点:
- 允许批量创建对象,减少代码重复。
弊端:
- 在打印对象时,对象的类型都是
Object
类型,不能明确区分出用哪个工厂函数创建的对象。
3. 构造函数
构造函数在JavaScript中扮演了其他语言中类的角色,通过new
关键字创建对象。
Java中的构造函数:
java
public class Person {
Person() { // 构造函数
System.out.println("无参的构造方法");
}
}
JavaScript中的构造函数和类:
- ES5方式:
javascript
function Person(name, age) { // 构造函数
this.name = name;
this.age = age;
}
const p1 = new Person('w', 15);
const p2 = new Person('ww', 155);
- ES6方式:
javascript
class Person {
constructor(name, age) { // 构造函数
this.name = name;
this.age = age;
}
}
const p1 = new Person('w', 15);
const p2 = new Person('ww', 155);
优点:
- 使用
new
关键字创建对象时,能够明确对象的类型。 - 支持面向对象编程的很多特性(如继承、方法定义等)。
手动实现一个 new 语法糖
利用工厂函数创建对象,因此可以发现下面手动实现一个new ,其中new 很像一个构造函数语法糖
js
function createPerson(name,age){
const per = {}
per.name = name
per.age =age
return per
}
const p1 = createPerson('w',15)
const p2 = createPerson('ww',155)
了解 Object.create
Object.create(proto,[propertiesObject])
静态方法以一个现有对象作为原型,创建一个新对象,这是一种更直接地基于原型链实现继承的方式。它允许我们创建一个新对象,并将其原型直接指定为传入的对象。这种方式绕过了构造函数,为原型继承提供了一种更为灵活和低级别的控制。
proto
:要作为新对象原型的对象,或 null。propertiesObject
(可选):包含一个或多个属性描述符的对象,这些属性描述符将被添加到新对象中。这些属性对应于 Object.defineProperties() 的第二个参数
通过 Object.create
实现原理其实可以看出来 Object.create(proto, [propertiesObject])
创建的新对象的内部 [[Prototype]]
其实链接的是传入参数 proto
js
// 模拟一个 Object.create
function simpleCreate(proto, props) {
// 创建一个空对象
const obj = {};
// 设置新对象的原型为传入的proto
if (typeof proto === 'object' || typeof proto === 'function') {
obj.__proto__ = proto;
} else {
throw new TypeError('Object prototype may only be an Object or null');
}
原型链挂在对象
当你使用 Object.create(proto)
时,所创建的新对象具有对 proto 的引用,它作为新对象的原型。在这个新对象上调用任何属性或方法时,如果该对象自身没有这个属性或方法,JavaScript 引擎会在它的原型(即 proto)中查找
dog 继承了 animal 可以用 animal 上的属性和方法
js
const animal = {
eats: true,
name:'123'
};
const dog = Object.create(animal);
dog.name = '456'
console.log(dog.name); // 456 指向自身的 并不会影响到原型链上的
console.log(animal.name); // 123 并不会改变原型链上的
console.log(dog.eats); // true, 因为在 dog 的原型链上有 eats 属性
console.log(dog.__proto__); // { eats: true }, 即 dog 的 __proto__ 是 animal 对象
console.log(Object.getPrototypeOf(dog) === animal); // true, 验证 dog 的原型是 animal
创建出来的dog 对象 dog.__proto__.animal.__proto__.Object
挂在到构造函数上
js
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
// 这里让o2对象的原型链指向了Person,并且给o2对象自己添加了个属性p
/*
下面的写法等同于
const o2 = new Object()
o2.__proto__ = Person.prototype
o2.p = 42
所以o2只是继承了Person.prototype原型链但是没有基础他的name属性
因此o2是没有name属性的
*/
o2 = Object.create(Person.prototype, {
p: {
value: 42,
writable: true,
enumerable: true,
configurable: true
}
});
// 打印结果如图一
console.log(o2)
const per = new Person()
o3 = Object.create(per, {
p: {
value: 42,
writable: true,
enumerable: true,
configurable: true
}
});
// 打印结果如图一
console.log(o3)
创建空对象
有时候你可能需要创建一个没有任何原型链(尤其是没有默认的 Object 原型链)的对象,这可以通过传入 null 来实现。
js
const pureObject = Object.create(null);
pureObject.someProp = 'some value';
console.log(pureObject); // { someProp: 'some value' }
console.log(Object.getPrototypeOf(pureObject)); // Output: null
实现一个new
手动实现主要遵守的四个步骤
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
设计时候要考虑到构造函数传参 new Person('wang')
因此对象设计 var a = objectFactory( Person, 'sven' )
js
function Person(name) {
// constructor.apply(obj, prams); 会将属性或方法添加到obj中
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
function objectFactory(...args) {
// 获取构造函数和传入的参数
const [constructor, ...prams] = args;
// 将对象的原型指向构造函数的原型 创建一个对象
const obj = Object.create(constructor.prototype);
// 执行构造函数,将属性或方法添加到obj中
const result = constructor.apply(obj, prams);
return typeof result === "object" ? result : obj; // 确保构造器总是会返回一个对象
}
// var objectFactory = function(){
// // 创建一个 空对象
// var obj = new Object();
// // 获取构造函数
// var Constructor = [].shift.call( arguments )
// // 改变当前obj 空对象原型链的指向
// obj.__proto__ = Constructor.prototype
// // 改变构造函数指向
// var ret = Constructor.apply( obj, arguments );
// return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
// }
// 这个写法等同 console.log(new Person('wang'))
var a = objectFactory(Person, "sven");
console.log(a);
console.log(a.name); // 输出:sven
console.log(a.getName()); // 输出:sven
console.log(Object.getPrototypeOf(a) === Person.prototype);
对这个做一个说明 typeof result === "object" ? result : obj;
- 当构造函数有返回值的时候, return 出来的是一个和 this 无关的对象时,new 命令会直接返回这个新对象,而不是通过 new 执行步骤生成的 this 对象
js
function Person(){
this.name = 'Jack';
return {age: 18}
}
var p = new Person();
console.log(p) // {age: 18}
console.log(p.name) // undefined
console.log(p.age) // 18
当返回时候是非对象,那么它还是会根据 new 关键词的执行逻辑,生成一个新的对象(绑定了最新 this),最后返回出来
js
function Person(){
this.name = 'Jack';
return 'tom';
}
var p = new Person();
console.log(p) // {name: 'Jack'}
console.log(p.name) // Jack
ES6 和 ES5 的写法比较
ES6 出现了class 语法糖更加方便让 js 可以创建对象
- ES5 中类的写法:
javascript
function PersonType(name) {
this.name = name;
}
PersonType.prototype.sayName = function () {
console.log(this.name);
};
var personType = new PersonType('wang');
personType.sayName(); // wang
console.log('PersonType instanceof PersonType:', personType instanceof PersonType); // true
console.log('PersonType instanceof Object:', personType instanceof Object); // true
- ES6 中类的写法:
javascript
class PersonClass {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
let personClass = new PersonClass('wang');
personClass.sayName(); // wang
console.log('PersonClass instanceof PersonClass:', personClass instanceof PersonClass); // true
console.log('PersonClass instanceof Object:', personClass instanceof Object); // true
console.log('typeof PersonClass:', typeof PersonClass); // function
ES6 和 ES5 类的其他不同点
-
函数声明提升 vs. 类声明不可提升:
- 函数声明可以被提升,类声明类似
let
声明,不能被提升。 - 声明前使用类会导致报错。
- 函数声明可以被提升,类声明类似
-
自动严格模式:
- 类声明的所有代码自动运行在严格模式下。
-
方法不可枚举:
- 在 ES5 中需要手动指定方法不可枚举。
- ES6 类方法默认不可枚举,挂在对象原型上。
-
[[Construct]]
方法:- 通过
new
调用不含[[Construct]]
的方法会报错。例如箭头函数
- 通过
-
构造函数必须使用
new
调用:- 直接调用类的构造函数会抛出错误。
-
类名不可修改:
- 类声明结束后类名依然可以修改,但在类中修改会报错。
javascript
class Foo {
constructor() {
Foo = "bar"; // 这里会抛出错误
}
}
Foo = "bar"; // 类声明结束后才可以修改类名
代码优化与实现
javascript
// ES5 版本的符合以上特性的类实现
let PersonType2 = (function () {
"use strict";
const PersonType2 = function (name) {
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
};
Object.defineProperty(PersonType2.prototype, "sayName", {
value: function () {
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonType2;
}());
const a = new PersonType2('wang');
console.log(a.name); // wang
访问器属性写法比较
- ES5 实现:
javascript
let CustomHTMLElement = (function() {
"use strict";
const CustomHTMLElement = function(element) {
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.element = element;
}
Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: true,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});
return CustomHTMLElement;
}());
- ES6 类实现:
javascript
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false
优化建议
- 在大型项目中,使用 ES6 类可以提高代码的可读性和可维护性。
- 尽量在类的构造函数中定义所有实例属性,避免属性混乱。
- 避免在类中直接修改类名,确保代码逻辑清晰。
- 使用
Object.defineProperty
管理 ES5 访问器属性,确保代码在未来的 ES6 迁移中更简便。