JavaScript:构造函数&面向对象
- 构造函数
- 实例化
- 静态成员
- 实例成员
- 内置构造函数
- 引用类型
- 基本含义
- 常用属性方法
- Object
- Array
- 包装类型
- 基本含义
- 常用属性方法
- String
- Number
- 面向对象
- 原型对象
- constructor
- 对象原型
- 原型链
- 原型继承
构造函数
在讲解构造函数之前,我们来看一个案例:
请创建四个对象,包括爸爸、妈妈、弟弟、妹妹四人,每个人要有name
,age
,gender
三个属性。
示例:
const father = {
name: "张三",
age: 49,
gender: "男"
}
const mother = {
name: "翠花",
age: 47,
gender: "女"
}
const brother = {
name: "张四",
age: 16,
gender: "男"
}
const sister = {
name: "张妞",
age: 12,
gender: "女"
}
有没有发现,在创建对象时,这四个对象十分类似,最后的语句显得重复性很高。
构造函数是一种特殊的函数,可以用来快速创建相似的对象。
构造函数的基本格式如下:
function ConstructorName(parameter1, parameter2, ...) {
// 构造函数的代码
this.property1 = value1;
this.property2 = value2;
...
this.method1 = function() {
// 方法1的代码
}
this.method2 = function() {
// 方法2的代码
}
...
}
在构造函数中,可以定义需要的属性和方法。属性可以通过this
关键字来定义,并且可以在构造函数外部访问。方法也可以通过this
关键字来定义,并且可以在构造函数外部调用。
那么创建好了一个构造函数,要如何调用它呢?
通过调用构造函数时使用new
关键字,可以创建一个该构造函数的实例。例如:
var obj = new ConstructorName(argument1, argument2, ...);
其中,argument1, argument2, ...
为传递给构造函数的参数。构造函数执行后返回一个新的对象实例。
构造函数在使用function
关键字声明时,函数名的首字母通常大写,以区别于普通函数。
- 我们利用一个案例来加强说明:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
}
}
var person1 = new Person("John", 25);
person1.sayHello(); // 输出:Hello, my name is John and I am 25 years old.
在上面的例子中,
Person
是一个构造函数,它接受两个参数name
和age
。这两个参数与最后得到的对象的属性值一一对应。
通过使用new
关键字和Person
构造函数,我们创建了一个名为person1
的新对象,并将它的name
属性设置为"John",age
属性设置为25。
还定义了一个名为sayHello
的方法,该方法可以向控制台输出一个问候语,包含对象的name
和age
属性。
最后,我们通过调用person1.sayHello()
方法来输出问候语。
构造函数允许我们创建多个具有相同属性和方法的对象,通过传递不同的参数值来设置它们的属性值。
那么我们尝试构建最初的一家四口的构造函数:
function Family(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender
}
const father = new Family("张三", 49, '男');
const mother = new Family("翠花", 47, '女');
const brother = new Family("张四", 16, '男');
const sister = new Family("张妞", 12, '女');
通过这个构造函数的方式,我们就可以快速创建大量相似的对象了。
实例化
使用new关键字调用函数,这个过程就叫做实例化。接下来我为大家讲解实例化的执行过程,帮助理解:
在JavaScript中,"new"关键字用于创建一个对象实例。当使用"new"关键字调用一个函数时,JavaScript引擎会执行以下操作:
- 创建一个新的空对象。
- 将空对象的原型设置为构造函数的原型。
- 将构造函数的作用域赋给新对象(即将构造函数中的"this"指向新对象)。
- 执行构造函数,并将构造函数中的属性和方法添加到新对象中。
- 返回新对象。
示例代码如下所示:
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("John", 25);
console.log(person.name); // 输出 "John"
console.log(person.age); // 输出 25
在上面的示例中,其执行过程如下:
- 使用"new"关键字创建一个"person"对象
由于通过new调用了一个函数,此时new会创建一个名为person的对象
- 然后将空对象的原型设置为"Person"函数的原型。
这个原型我们后续会讲解。
- 接下来,JavaScript引擎将构造函数中的"this"指向新对象
这一步非常重要,由于我们在写构造函数时,都是this.属性
所以此处的this
要转化为新创建的对象,才能保证我们的属性是加在对象上的。
- 将"name"和"age"属性添加到新对象中。
上一步我们已经将this转化为了新对象,后续执行代码this.name = name
this.age = age
其实就是给对象添加属性的过程。
- 最后,“new"关键字返回新对象,我们将其赋值给变量"person”。
当一个对象创建好后,就会作为函数的返回值返回,后续才可以使用这个变量。
静态成员
对象的静态成员是指直接与对象构造函数相关联的属性和方法,而不是对象的实例。静态成员可以通过对象构造函数的名称直接访问。
可以简单理解为,直接对构造函数添加到属性和方法,就是静态成员。
比如:
function Person(name, age) {
}
Person.eyes = 2;
Person.arms = 2;
eyes
和arms
属性是直接添加到了构造函数本身上,也就是通过函数名Person
直接添加,而不是实例对象上。因此,它们被视为构造函数的静态成员。
实例成员
JavaScript的实例成员是指在构造函数中使用this关键字定义的成员。
例如:
function Person(name, age) {
// 实例属性
this.name = name;
this.age = age;
}
// 创建对象实例
var person1 = new Person("Alice", 25);
var person2 = new Person("Bob", 30);
person1.gender = '男';
person2.gender = '女';
在上面的示例中,Person
构造函数中使用this
关键字定义了实例属性name
和age
。那么name
和age
都算实例成员。
但是gender
属性并非在构造函数中定义的实例属性,而是在实例化后通过给实例直接添加的属性。因此,gender
属性是实例的自有属性,不是实例成员。
内置构造函数
在我们先前的JavaScript学习中,我们可以发现不仅仅只有对象可以使用方法和属性,而很多其它数据也可以。
比如str.length
arr.map()
为什么不是对象也可以用对象才有的语法呢?
这是因为字符串(String),数值(Number),布尔值(Boolean)等等的大部分数据,都有专门的构造函数。
这也就是为什么我们可以new
一个数组,因为数组也有自己的构造函数。
引用类型
基本含义
在JavaScript中,引用类型是一种用于存储对象的数据类型。它允许开发人员创建和操作复杂的数据结构,如数组、对象和函数等。
具体的引用类型有:
- 对象(Object):对象是一种无序的键值对集合,可以包含不同数据类型的属性。
- 数组(Array):数组是一种有序的集合,可以存储多个值,并使用索引来访问和修改这些值。
- 函数(Function):函数是一段可执行的 JavaScript 代码块,可以被调用和重复使用。
- 日期(Date):日期类型用于表示日期和时间。
- 正则表达式(RegExp):正则表达式类型用于匹配字符串的模式。
除了以上几种引用类型,还有一些其他的内置对象,例如:Math、JSON、Promise等。这些对象提供了一些特定的方法和属性,以便于开发人员处理和操作数据。
常用属性方法
Object
Object
是内置的构造函数,用于创建普通对象。
// 通过构造函数创建普通对象
const user = new Object({name: '小明', age: 15})
// 这种方式声明的变量称为【字面量】
let student = {name: '杜子腾', age: 21}
总结:
- 推荐使用字面量方式声明对象,而不是
Object
构造函数Object.assign
静态方法创建新的对象Object.keys
静态方法获取对象中所有属性Object.values
表态方法获取对象中所有属性值
Array
Array
是内置的构造函数,用于创建数组。
// 构造函数创建数组
let arr = new Array(5, 7, 8);
// 字面量方式创建数组
let list = ['html', 'css', 'javascript']
总结:
- 推荐使用字面量方式声明数组,而不是
Array
构造函数- 实例方法
forEach
用于遍历数组,替代for
循环 (重点)- 实例方法
filter
过滤数组单元值,生成新数组(重点)- 实例方法
map
迭代原数组,生成新数组(重点)- 实例方法
join
数组元素拼接为字符串,返回字符串(重点)- 实例方法
find
查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)- 实例方法
every
检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)- 实例方法
some
检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false- 实例方法
concat
合并两个数组,返回生成新数组- 实例方法
sort
对原数组单元值排序- 实例方法
splice
删除或替换原数组单元- 实例方法
reverse
反转数组- 实例方法
findIndex
查找元素的索引值
包装类型
基本含义
JavaScript中的包装类型是指原始数据类型(Number、String、Boolean)的对应的对象类型(Number Object、String Object、Boolean Object)。例如,当我们使用"hello"这个字符串时,实际上在底层会自动创建一个String对象来表示该字符串。这些包装类型提供了一些额外的属性和方法,使得原始数据类型可以像对象一样进行操作。
常用属性方法
String
String
是内置的构造函数,用于创建字符串。
// 使用构造函数创建字符串
let str = new String('hello world!');
// 字面量创建字符串
let str2 = '你好,世界!';
总结:
- 实例属性
length
用来获取字符串的度长(重点)- 实例方法
split('分隔符')
用来将字符串拆分成数组(重点)- 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])
用于字符串截取(重点)- 实例方法
startsWith(检测字符串[, 检测位置索引号])
检测是否以某字符开头(重点)- 实例方法
includes(搜索的字符串[, 检测位置索引号])
判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)- 实例方法
toUpperCase
用于将字母转换成大写- 实例方法
toLowerCase
用于将就转换成小写- 实例方法
indexOf
检测是否包含某字符- 实例方法
endsWith
检测是否以某字符结尾- 实例方法
replace
用于替换字符串,支持正则匹配- 实例方法
match
用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
Number
Number
是内置的构造函数,用于创建数值。
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
总结:
- 推荐使用字面量方式声明数值,而不是
Number
构造函数- 实例方法
toFixed
用于设置保留小数位的长度
面向对象
JavaScript的面向对象思想是基于构造函数的,没有构造函数,JavaScript也就没有面向对象了。接下来我为大家讲解JavaScript在面向对象中的一些概念及其使用。
在讲解之前,先给大家一个问题:
在构造函数时,我们也许会需要某一类对象都可以干某件事情。比如说每个人(假设是person对象)都可以说:“Hi!”。那么我们就要执行以下语句:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender
}
const father = new Person("张三", 49, '男');
const mother = new Person("翠花", 47, '女');
father.sayHi = function() {console.log("Hi!")};
mother.sayHi = function() {console.log("Hi!")};
这个过程中,我们创建了两个sayHi
方法,分别放在了mother
和father
内部。
我们运行代码:
console.log(father.sayHi === mother.sayHi);
输出结果为false
,说明两个sayHi
函数是不一样的,这就会导致内存的浪费。但是两者功能完全一致,我们有没有方法让同一个类的对象可以调用同一个函数,即所有的person
都可以使用同一个sayHi
函数?
讲解完以下内容,我们就可以解决这个问题了。
原型对象
JavaScript中的每个对象都有一个原型对象prototype
,它是一个用于继承属性和方法的对象。
构造函数通过原型对象分配的函数是所有对象共享的,对于同一个构造函数的实例,它们的原型是同样的。
- JavaScript 规定,每一个构造函数都有一个
prototype
属性,指向另一个对象,所以我们也称为原型对象 - 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
- 构造函数和原型对象中的this 都指向 实例化的对象
简而言之就是,对于同一个构造函数的实例,它们的原型对象是共享的。当我们把某个属性或者方法放在这个构造函数的
prototype
原型对象下方,那么这个构造函数的所有实例都可以调用这个属性或者方法。
了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:
<script>
function Person() {
// 此处未定义任何方法
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}
// 实例化
let p1 = new Person();
p1.sayHi(); // 输出结果为 Hi~
</script>
其中我们通过 Person.prototype.sayHi = function () {}
做到了所有person下方的都可以调用sayHi函数,因为其被定义在了原型对象 prototype
内部。
机制讲解:
构造函数 Person
中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi
,接下来改动一下代码:
<script>
function Person() {
// 此处定义同名方法 sayHi
this.sayHi = function () {
console.log('嗨!');
}
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~');
}
let p1 = new Person();
p1.sayHi(); // 输出结果为 嗨!
</script>
构造函数 Person
中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi
。
通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象查找,然后再去原型对象查找,并且原型对象被所有实例共享。
<script>
function Person() {
// 此处定义同名方法 sayHi
this.sayHi = function () {
console.log('嗨!' + this.name)
}
}
// 为构造函数的原型对象添加方法
Person.prototype.sayHi = function () {
console.log('Hi~' + this.name)
}
// 在构造函数的原型对象上添加属性
Person.prototype.name = '小明'
let p1 = new Person()
p1.sayHi(); // 输出结果为 嗨!
let p2 = new Person()
p2.sayHi()
</script>
通过原型对象,我们可以实现对象之间的继承和共享。当我们在原型对象上定义一个属性或方法时,所有继承自该原型对象的对象实例都可以访问和使用这个属性或方法。这样可以减少内存消耗,提高代码的复用性
总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。
constructor
constructor
是一个属性,其处于原型对象中。
constructor
的作用是找到构造函数。
那么这个constructor
有什么用呢?
我们看到一段代码:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender
}
Person.prototype = {//给Person.prototype添加sing和dance方法
sing: function () {console.log("唱歌")},
dance: function () {console.log("跳舞")}
}
Person.prototype = {//给Person.prototype添加cook和clean方法
cook: function () {console.log("做饭")},
clean: function () {console.log("清洁")},
}
const father = new Person("张三", 49, '男');
father.sing();//错误,sing函数未定义
father.cook();//正常输出
当我们需要对prototype
一次性添加多个属性方法时,如果直接Person.prototype = {}
,那么后面的属性和方法救护把前面的属性方法覆盖掉,导致原先的方法丢失了。此时我们就可以通过constructor
指回原先的构造函数,将其原先的属性和方法保留的前提下,再新增属性方法。
比如这样:
Person.prototype = {
constructor: Person,
sing: function () {console.log("唱歌")},
dance: function () {console.log("跳舞")}
}
此时constructor就会将原先prototype
有的属性方法保留再添加其它的属性。
在对prototype
批量赋值时,千万不要忘记在开头补上constructor: Person
。
思考一个问题:
构造函数可以创建实例对象,构造函数还有一个原型对象,一些公共的属性或者方法放到这个原型对象身上。但是为啥实例对象可以访问原型对象里面的属性和方法呢?
如下图:
我们的实例对象好像无法访问到prototype
,那为什么实例对象可以使用prototype
中的属性和方法呢?
这就涉及到了对象原型__proto__
。
对象原型
先辨析一个问题:
名称 | 对象名 |
---|---|
prototype | 原型对象 |
__proto__ | 对象原型 |
千万不要把两者搞混了!!!
对象都会有一个
__proto__
对象原型 指向prototype
原型对象。
可以通过__proto__
原型对象 来访问对象原型。原型对象会包含对象的共享属性和方法,所以实例对象可以通过__proto__
对象原型来访问prototype
原型对象。
此外,__proto__
对象原型 内部也有一个constructor
属性来指向构造函数。
至此,我们就得到一个较完整的关系网:
这个关系网将构造函数与实例对象联系了起来,并且为他们创建了一个可以共享属性与方法的对象prototype
。它们之间可以通过各种属性访问,比如prototype
,__proto__
以及constructor
。
原型链
JavaScript的原型链是一种机制,用于实现继承和属性查找。在JavaScript中,每个对象都有一个原型prototype
属性,可以通过它访问另一个对象。而每个对象的原型又有自己的原型,形成了一个原型链。原型链的顶端是Object.prototype
对象,它是所有对象的根。
当访问一个对象的属性时,JavaScript首先在对象自身中查找,如果找到则返回该属性的值。如果没有找到,它会继续在对象原型
prototype
上查找,如果还是没有找到,它会再继续在原型的原型(一般为Object原型
)上查找,直到找到属性或者到达原型链的顶端为止。如果最终都没有找到,那么返回undefined
。
在这个过程中,为了访问到上一级的prototype
,我们就要利用__proto__
。相当于__proto__
为寻找prototyp
提供了一条路线。
我们观察上图的查找过程会发现,整个查找prototype
的路程中,都是由__proto__
引导的。
这种原型链的机制使得对象可以继承其原型对象的属性和方法。当我们访问一个对象的方法时,JavaScript会在原型链上查找并返回找到的方法。这意味着,如果一个对象的原型对象上定义了一个方法,那么所有继承自该原型对象的对象都可以访问并使用该方法。
通过原型链,JavaScript实现了一种灵活而高效的继承机制,使得对象可以方便地共享和继承属性和方法。同时,原型链也允许在运行时动态地添加、修改和删除对象的属性和方法,使得JavaScript具有了一定的灵活性和扩展性。
原型继承
在JavaScript中,继承可以通过原型链来实现。每个JavaScript对象都有一个原型对象,它定义了对象的属性和方法。继承就是指一个对象可以继承另一个对象的属性和方法。
原型链继承:通过将子类的原型对象指向父类的实例,实现继承。
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
console.log('Hello, I am ' + this.name);
};
function Child() {
this.name = 'Child';
}
Child.prototype = new Parent(); // 将子类的原型对象指向父类的实例
var child = new Child();
child.sayHello(); // 输出:Hello, I am Child
在这个例子中,创建了一个Parent
构造函数,并在其原型对象上定义了一个sayHello
方法。然后创建了一个Child
构造函数,并将其原型对象prototype
指向一个Parent
的实例。这样,Child
的实例就能够继承Parent
的属性和方法。