JavaScript青少年简明教程:面向对象编程入门
JavaScript 支持多种编程范式(programming paradigms),即支持多种不同的编程风格和方法。
前面介绍的编程方式,主要是面向过程编程(Procedural Programming)它关注于通过过程(或称为函数、子程序)来组织程序的逻辑。面向过程编程强调将程序分解成多个过程,这些过程以顺序执行的方式处理数据和操作。它的核心思想是将程序分成多个模块(过程)来处理特定任务,常常涉及到数据和操作的分离。它是命令式编程(Imperative Programming)的一种具体风格。
【命令式编程(Imperative Programming)是一个广泛的范式,关注如何通过一系列的步骤来改变程序的状态。
面向过程编程(Procedural Programming)是命令式编程的一种具体实现,强调通过过程(函数)来组织和管理程序的逻辑。】
JavaScript还支持面向对象编程(Object-Oriented Programming, OOP)、函数式编程(Functional Programming)、事件驱动编程(Event-Driven Programming)等。
【面向对象编程通过封装数据和行为到对象中来组织代码。
事件驱动编程基于事件的触发和处理,常用于用户界面的交互和异步操作。
函数式编程将计算视为函数的应用,注重避免副作用和状态改变,函数是头等公民。
通过支持多种编程风格和方法,允许开发者选择最适合他们问题的解决方案,或混合使用这些范式,以创建高效、可维护的代码。】
初学者开始重点学习关注的编程范式,一般而言是面向过程编程和面向对象编程。本节简要介绍JavaScript 面向对象编程知识。
JavaScript 的面向对象编程特性与传统的面向对象编程语言(如 Java 或 C++)有一些不同,但它提供了足够的功能来实现复杂的面向对象设计。
在 JavaScript 中,几乎所有的数据类型都是对象,或可以看作是对象。对象是具有属性(键-值对)和方法(函数)的数据结构。
JavaScript 使用原型链而不是类来实现继承。每个对象都有一个内部链接指向另一个对象,称为它的原型。ES6 引入了 class 关键字,提供了一种更接近传统 OOP 语言的语法糖,但底层仍然基于原型。
特别提示:一些讲解资料对Property和attribute翻译,有点太随意或混乱,为避免误解,特此予先说明。在 Web 开发中,attribute 通常指 HTML 元素的属性,而 property 则更多指 DOM 元素的属性。在 JavaScript 中,将 Property 译为属性、attribute 译为特性是比较常见的做法。 关于JavaScript的Property 权威链接 https://developer.mozilla.org/en-US/docs/Glossary/Property/JavaScript
也可以参看,编程语言中的Class、Object、Property、attribute、field、Method、Event https://blog.csdn.net/cnds123/article/details/106031884
JavaScript 的对象
对象(Object) 是 JavaScript 的一种数据类型。它用于存储各种键值集合和更复杂的实体。可以通过 Object() 构造函数或者使用对象字面量的方式创建对象。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
对象则用来存储键值(key: value)对也称为名称-值(name-value)对的集合,其中的值可能是数据或函数。键和值之间用冒号分隔,键值对之间用逗号分隔,整个对象被大括号包围。也有人说,对象是属性(properties)的无序集合,每个属性都有一个名称和一个值。
例如:
let obj = { name: 'John', age: 30 };
在这个例子中,'name’和’age’是对象的键,'John’和30是对应的值。
【对象可以被看作是属性(键值对)的集合,其中每个属性都有一个名称和一个对应的值。这些值可以是数据类型(比如字符串、数字、布尔值等),也可以是函数(称为方法)。对象中的属性是无序的,可以通过属性名来访问和操作对应的值。
对象的表示方式是使用大括号 {} 包围键值对,每个键值对之间使用逗号 , 分隔。键和值之间使用冒号 : 分隔。】
JavaScript 对象是动态的——属性(properties)通常可以添加和删除。这一点和C++、Java这类传统的面向对象编程语言需要先定义类再定义对象的做法不同,直到ES6(ECMAScript 6,ECMAScript 2015 )发布之前 JavaScript没有定义类的语法。
可以使用以下方式添加属性:
使用点(.)操作符:obj.property = value;
使用方括号([])操作符:obj["property"] = value;
可以使用以下方式删除属性:
使用delete关键字:delete obj.property;
使用delete关键字和方括号操作符:delete obj["property"];
例如:
let person = { name: "John", age: 20 }; person.gender = "Male"; // 添加属性 console.log(person); // {name: "John", age: 20, gender: "Male"} delete person.age; // 删除属性 console.log(person); // {name: "John", gender: "Male"}
注意,访问不存在的属性将返回undefined。
JavaScript对象除了维护自己的属性集,还继承另一个对象的属性,即它的“原型”(prototype)。这种“原型继承”是 JavaScript 的一个关键特性(特征、feature)。JavaScript中区分直接在对象上定义的属性和从原型对象继承的属性是很重要的。JavaScript 使用术语“自有属性”来指代非继承属性。
原型链是 JavaScript 中实现继承的核心概念。当访问一个对象的属性时,JavaScript 引擎首先在该对象本身查找,如果找不到,它会沿着原型链向上查找,直到找到该属性或到达原型链的末端。
原型链的顶端是 Object.prototype,所有对象都直接或间接地继承自它。当你使用 new 操作符创建一个新对象时,这个对象的内部属性 [[Prototype]] 会指向其构造函数的原型对象。这样,所有实例对象都会连接到同一个原型对象,形成一个原型链。
可以使用hasOwnProperty()方法来区分自有属性和继承属性。
例如:
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log("Hello, " + this.name + "!"); }; let person = new Person("John"); person.sayHello(); // 输出:Hello, John! console.log(person.hasOwnProperty("name")); // 输出:true console.log(person.hasOwnProperty("sayHello")); // 输出:false
在上面的例子中,Person是一个构造函数,person是通过该构造函数创建的对象。name属性是在对象上直接定义的,因此它是一个自有属性。sayHello方法是定义在原型对象上的,因此它是一个继承属性。
前面提到过,JavaScript中的对象类型包括:
1、普通对象(Plain objects):是指用户自定义的对象类型。这些对象通常是通过使用构造函数或类来定义的。在JavaScript中,有多种方式可以创建对象。在ES6及以后的版本中,JavaScript引入了类(class),这是一种创建和定义对象的新方法。
2、数组(Arrays):例如let arr = [1, 2, 3]; 数组对象有一些特殊的属性和方法,如length、push、pop等。
3、函数(Functions):函数也是对象,它们可以有属性和方法。例如:
function sayHello() {
console.log('Hello');
}
sayHello.myProperty = 'This is a property of the function.';
4、日期(Dates):例如let date = new Date();
5、正则表达式(RegExp,Regular expressions):例如let regex = /ab+c/;
6、其他内置对象,如Math,Set,Map等。
【在JavaScript中,数组和函数确(详见以前的节)实是特殊类型的对象,也就是说,它们也是键值对的集合,但是它们的行为和普通对象有所不同。
数组:数组是一种特殊的对象,它的键是基于零的数字索引,而且它有一些额外的属性和方法,如length属性和push方法,这使得数组在处理有序的数据集合时更加方便。但是,你也可以给数组添加其他类型的键和值,就像给普通对象一样:
let arr = [1, 2, 3];
arr['myKey'] = 'myValue';
console.log(arr['myKey']); // 输出:'myValue'
函数:函数也是一种特殊的对象,它们可以有自己的属性和方法。这意味着你可以将函数赋值给变量,将它们作为其他函数的参数,或者将它们作为对象的属性。函数的键是它的属性名,值可以是任何类型的数据:
function sayHello() {
console.log('Hello');
}
sayHello.myProperty = 'This is a property of the function.';
console.log(sayHello.myProperty); // 输出:'This is a property of the function.'
所以,虽然数组和函数在JavaScript中是特殊类型的对象,但它们仍然是键值对的集合。】
在JavaScript中,有多种方式可以创建对象,常用的创建的方式有
1.利用字面量创建对象
对象字面量:就是花括号 { } 里面包含了表达这个具体事物(对象)的属性和方法。
{ } 里面采取键值对的形式表示
键:相当于属性名
值:相当于属性值,可以是任意类型的值(数字类型、字符串类型、布尔类型,函数类型等)。
例如:
let star = { name : 'pink', age : 18, sex : '男', sayHi : function(){ alert('大家好啊~'); } };
2.利用new Object创建对象
Object() :第一个字母大写
new Object() :需要 new 关键字
使用的格式:对象.属性 = 值;
例如:
let andy = new Obect(); andy.name = 'pink'; andy.age = 18; andy.sex = '男'; andy.sayHi = function(){ alert('大家好啊~'); }
3.利用构造函数创建对象
构造函数 :是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与 new 运算符一起使用。我们可以把对象中一些==公共的属性和方法==抽取出来,然后封装到这个函数里面。
使用构造函数要时要注意以下两点:
构造函数用于创建某一类对象,按约定其首字母要大写
构造函数要和 new 一起使用才有意义。
例如:
function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.sayHi = function() { alert('我的名字叫:' + this.name + ',年龄:' + this.age + ',性别:' + this.sex); } } let bigbai = new Person('大白', 100, '男'); let smallbai = new Person('小白', 21, '男'); console.log(bigbai.name); console.log(smallbai.name);
4.在ES6及以后的版本中,JavaScript引入了类(class),这是一种创建和定义对象的新方法。它们与传统的构造函数类似,但是提供了更简洁的语法和更多的功能。在JavaScript中,可以使用class关键字创建一个类,然后使用new关键字实例化该类。JavaScript 中的类(class)https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes
例如:
class Person { constructor(name, age) { this.name = name; this.age = age; } sayHello() { console.log(`我的名字叫${this.name},年龄${this.age}岁。`); } } let person1 = new Person("Alice", 30); person1.sayHello(); // 输出:我的名字叫Alice,年龄30岁。
下面重点介绍这种方式。
JavaScript 中的类和对象
JavaScript 作为一种面向对象的编程语言,也支持类和对象的概念。尽管在早期的版本中,JavaScript 主要通过原型继承实现面向对象编程,但自 ECMAScript 6(ES6)起,JavaScript 引入了类语法,使得面向对象编程更加直观和易用。与传统的基于原型的继承不同,ES6 的类语法更加简洁和易于理解,但本质上仍然是基于原型的。这使得 JavaScript 在支持面向对象编程的同时,仍保留了其灵活的原型继承机制。
以下是对 JavaScript 中类和对象的介绍。
类(Class)
类 是创建对象的蓝图或模板,定义了对象的属性和方法。
定义类的语法:
class ClassName {
// 构造函数
constructor(parameter1, parameter2) {
this.attribute1 = parameter1;
this.attribute2 = parameter2;
}
// 实例方法
method1() {
// 方法体
}
// 静态方法
static staticMethod() {
// 方法体
}
}
对象(Object)
对象 是类的实例,通过类的构造函数创建。每个对象都有自己独特的属性和方法。
创建对象的语法:
const objectName = new ClassName(arguments);
类和对象的属性和方法说明
实例属性:在构造函数中定义,用 this 引用。每个对象都有自己独立的实例属性。
实例方法:在类内部定义的方法,用 this 引用实例属性。
静态方法:也称为类属性,使用 static 关键字定义,不依赖于具体实例,通常用于实现与实例无关的功能。通过类名来访问,而不是通过实例,格式:MyClass.myStaticProperty
JavaScript 类中主要成员的用途和注意事项:
☆构造函数(Constructor)
用途:
初始化新创建的对象
设置对象的初始属性
注意事项:
使用 constructor 关键字定义
创建对象时自动调用
每个类只能有一个构造函数
如:
class Person { constructor(name, age) { this.name = name; this.age = age; } }
☆实例属性(Instance Properties)
用途:
存储对象的特定数据
注意事项:
通常在构造函数中使用 this 关键字定义
每个实例可以有不同的值
如:
class Car { constructor(make, model) { this.make = make; this.model = model; } }
☆实例方法(Instance Methods)
用途:
定义对象的行为
操作对象的数据
注意事项:
可以访问实例属性和其他实例方法
使用 this 关键字引用当前实例
如:
class Rectangle { constructor(width, height) { this.width = width; this.height = height; } getArea() { return this.width * this.height; } }
☆静态方法(Static Methods)
用途:
实现与类相关但不依赖于实例的功能
注意事项:
使用 static 关键字定义
通过类名调用,不能通过实例调用
不能直接访问实例属性或方法
如:
☆静态属性(Static Properties)
用途:
存储类级别的数据
所有实例共享的常量或配置
注意事项:
在类定义外使用 ClassName.propertyName 定义(早期语法)
在新的提案中,可以在类内部使用 static 关键字定义
通过类名访问,不能通过实例访问
如:
class Config { static API_URL = 'https://api.example.com'; } console.log(Config.API_URL);
☆Getter 和 Setter
用途:
控制属性的访问和修改
实现计算属性
注意事项:
使用 get 和 set 关键字定义
看起来像属性,但实际上是方法
可以进行值的验证或转换
如:
class Circle { constructor(radius) { this._radius = radius; } get diameter() { return this._radius * 2; } set radius(value) { if (value > 0) { this._radius = value; } } }
☆私有字段(Private Fields)
用途:
封装内部状态
防止外部直接访问和修改
注意事项:
使用 # 前缀定义,是 ECMAScript 的新特性
只能在类内部访问
如:
class BankAccount { #balance = 0; deposit(amount) { this.#balance += amount; } getBalance() { return this.#balance; } }
特别提示,JavaScript类的成员类型和概念很多,一次性全部掌握是有难度的。实际使用时有些部分不一定出现,选择合适的类型可以使您的代码更加清晰和高效。初学时,不需要一次掌握所有内容,可以先集中精力关注最常用的部分,您随着您编写更多的JavaScript代码,这些概念会变得越来越清晰。
下面给出一个简单而比较完整示例:
class Dog { constructor(name, age) { this.name = name; // 实例属性 this.age = age; } bark() { // 实例方法 return `${this.name} says woof!`; } static species() { // 静态方法 return 'Canis familiaris'; } } const dog1 = new Dog('Buddy', 3); console.log(dog1.bark()); // 输出: Buddy says woof! console.log(Dog.species()); // 输出: Canis familiaris
下面介绍封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)等内容,很重要,但涉及内容广泛,在此仅作简要介绍,作为初学者可以先作为了解内容。
继承(Inheritance)
继承允许一个类(子类)继承另一个类(父类)的属性和方法。
定义子类的语法:
class SubClass extends ParentClass {
// 子类定义
}
示例:
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound.`; } } class Dog extends Animal { constructor(name, age) { super(name); this.age = age; } speak() { return `${this.name} says woof!`; } } const dog = new Dog('Buddy', 3); console.log(dog.speak()); // 输出: Buddy says woof!
多态(Polymorphism)
多态性允许子类在继承父类的方法时提供自己的实现。子类继承父类,并重写父类的方法。
示例:
class Animal { speak() { console.log("动物发出声音"); } } class Dog extends Animal { speak() { console.log("汪汪!"); } } class Cat extends Animal { speak() { console.log("喵喵~"); } } const animal = new Animal(); const dog = new Dog(); const cat = new Cat(); animal.speak(); // 输出: 动物发出声音 dog.speak(); // 输出: 汪汪! cat.speak(); // 输出: 喵喵~
封装(Encapsulation)
指的是将数据(属性)和操作数据的方法(函数)封装在一个对象内部,从而隐藏内部实现细节,提供一个清晰的接口来与对象进行交互。
在现代 JavaScript 中(从 ES2022 开始),可以使用 # 前缀来定义私有属性和方法。这种方式提供了严格的封装,确保属性和方法只能在类内部访问。示例:
class Person { #name; // 私有属性 #age; constructor(name, age) { this.#name = name; this.#age = age; } getName() { return this.#name; } getAge() { return this.#age; } } const person = new Person('Alice', 30); console.log(person.getName()); // 输出: Alice console.log(person.getAge()); // 输出: 30 // console.log(person.#name); // 会报错,无法直接访问私有属性
Getter 和 Setter
JavaScript 中的 Getter 和 Setter可以使用 get 和 set 关键字定义。
Getter 和 Setter 是特殊的方法,允许你以更灵活的方式读取(get)和写入(set)对象的属性。它们提供了一种方式来定义“伪属性”,这些属性看起来像普通属性,但实际上是通过方法来访问和修改的。
Getter(获取器):
使用 get 关键字定义
不接受参数
用于读取属性值
Setter(设置器):
使用 set 关键字定义
接受一个参数(要设置的新值)
用于写入属性值
示例:
class Circle { constructor(radius) { this._radius = radius; // 约定使用下划线前缀表示内部属性 } // Getter get radius() { return this._radius; } // Setter set radius(value) { if (value <= 0) { throw new Error("半径必须为正数"); } this._radius = value; } // Getter get area() { return Math.PI * this._radius ** 2; } } const circle = new Circle(5); console.log(circle.radius); // 输出: 5 circle.radius = 10; // 使用 setter console.log(circle.radius); // 使用 getter,输出: 10 console.log(circle.area); // 使用 getter,输出: 约 314.16 circle.radius = -1; // 抛出错误:半径必须为正数
在这个例子中:
radius 有一个 getter 和 setter:
获取 radius 时,实际上调用了 get radius() 方法。
设置 radius 时,调用 set radius(value) 方法,允许我们在设置值之前进行验证。
area 只有一个 getter:
它是一个只读属性,每次访问时都会计算面积。
在使用时,getter 和 setter 看起来就像普通属性:
circle.radius = 10; // 使用 setter
console.log(circle.radius); // 使用 getter,输出: 10
console.log(circle.area); // 使用 getter,输出: 约 314.16
进一步学习可见:
JavaScript早期(ES6之前)对象及其的属性和方法的访问介绍 https://blog.csdn.net/cnds123/article/details/125406135
JavaScript面向对象编程再讲https://blog.csdn.net/cnds123/article/details/129982056
面向对象编程基本概念 https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object-oriented_programming