JavaScript:构造函数&面向对象

简介: JavaScript:构造函数&面向对象

构造函数

在讲解构造函数之前,我们来看一个案例:

请创建四个对象,包括爸爸、妈妈、弟弟、妹妹四人,每个人要有nameagegender三个属性。

示例:

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是一个构造函数,它接受两个参数nameage。这两个参数与最后得到的对象的属性值一一对应。

通过使用new关键字和Person构造函数,我们创建了一个名为person1的新对象,并将它的name属性设置为"John",age属性设置为25。

还定义了一个名为sayHello的方法,该方法可以向控制台输出一个问候语,包含对象的nameage属性。

最后,我们通过调用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引擎会执行以下操作:

  1. 创建一个新的空对象。
  2. 将空对象的原型设置为构造函数的原型。
  3. 将构造函数的作用域赋给新对象(即将构造函数中的"this"指向新对象)。
  4. 执行构造函数,并将构造函数中的属性和方法添加到新对象中。
  5. 返回新对象。

示例代码如下所示:

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 = namethis.age = age其实就是给对象添加属性的过程。

  • 最后,“new"关键字返回新对象,我们将其赋值给变量"person”。

当一个对象创建好后,就会作为函数的返回值返回,后续才可以使用这个变量。


静态成员

对象的静态成员是指直接与对象构造函数相关联的属性和方法,而不是对象的实例。静态成员可以通过对象构造函数的名称直接访问。

可以简单理解为,直接对构造函数添加到属性和方法,就是静态成员。

比如:

function Person(name, age) {
}
Person.eyes = 2;
Person.arms = 2;

eyesarms属性是直接添加到了构造函数本身上,也就是通过函数名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关键字定义了实例属性nameage。那么nameage都算实例成员。

但是gender属性并非在构造函数中定义的实例属性,而是在实例化后通过给实例直接添加的属性。因此,gender属性是实例的自有属性,不是实例成员。


内置构造函数

在我们先前的JavaScript学习中,我们可以发现不仅仅只有对象可以使用方法和属性,而很多其它数据也可以。

比如str.lengtharr.map()为什么不是对象也可以用对象才有的语法呢?

这是因为字符串(String),数值(Number),布尔值(Boolean)等等的大部分数据,都有专门的构造函数。

这也就是为什么我们可以new一个数组,因为数组也有自己的构造函数。

引用类型

基本含义

在JavaScript中,引用类型是一种用于存储对象的数据类型。它允许开发人员创建和操作复杂的数据结构,如数组、对象和函数等。

具体的引用类型有:

  1. 对象(Object):对象是一种无序的键值对集合,可以包含不同数据类型的属性。

  1. 数组(Array):数组是一种有序的集合,可以存储多个值,并使用索引来访问和修改这些值。

  1. 函数(Function):函数是一段可执行的 JavaScript 代码块,可以被调用和重复使用。

  1. 日期(Date):日期类型用于表示日期和时间。

  1. 正则表达式(RegExp):正则表达式类型用于匹配字符串的模式。

除了以上几种引用类型,还有一些其他的内置对象,例如:Math、JSON、Promise等。这些对象提供了一些特定的方法和属性,以便于开发人员处理和操作数据。

常用属性方法
Object

Object 是内置的构造函数,用于创建普通对象。

// 通过构造函数创建普通对象
  const user = new Object({name: '小明', age: 15})
  // 这种方式声明的变量称为【字面量】
  let student = {name: '杜子腾', age: 21}

总结:

  1. 推荐使用字面量方式声明对象,而不是 Object 构造函数
  2. Object.assign 静态方法创建新的对象
  3. Object.keys 静态方法获取对象中所有属性
  4. Object.values 表态方法获取对象中所有属性值

Array

Array 是内置的构造函数,用于创建数组。

// 构造函数创建数组
  let arr = new Array(5, 7, 8);
  // 字面量方式创建数组
  let list = ['html', 'css', 'javascript']

总结:

  1. 推荐使用字面量方式声明数组,而不是 Array 构造函数
  2. 实例方法 forEach 用于遍历数组,替代 for 循环 (重点)
  3. 实例方法 filter 过滤数组单元值,生成新数组(重点)
  4. 实例方法 map 迭代原数组,生成新数组(重点)
  5. 实例方法 join 数组元素拼接为字符串,返回字符串(重点)
  6. 实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)
  7. 实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)
  8. 实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false
  9. 实例方法 concat 合并两个数组,返回生成新数组
  10. 实例方法 sort 对原数组单元值排序
  11. 实例方法 splice 删除或替换原数组单元
  12. 实例方法 reverse 反转数组
  13. 实例方法 findIndex 查找元素的索引值

包装类型

基本含义

JavaScript中的包装类型是指原始数据类型(Number、String、Boolean)的对应的对象类型(Number Object、String Object、Boolean Object)。例如,当我们使用"hello"这个字符串时,实际上在底层会自动创建一个String对象来表示该字符串。这些包装类型提供了一些额外的属性和方法,使得原始数据类型可以像对象一样进行操作。


常用属性方法
String

String 是内置的构造函数,用于创建字符串。

// 使用构造函数创建字符串
  let str = new String('hello world!');
  // 字面量创建字符串
  let str2 = '你好,世界!';

总结:

  1. 实例属性 length 用来获取字符串的度长(重点)
  2. 实例方法 split('分隔符') 用来将字符串拆分成数组(重点)
  3. 实例方法 substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点)
  4. 实例方法 startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点)
  5. 实例方法 includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)
  6. 实例方法 toUpperCase 用于将字母转换成大写
  7. 实例方法 toLowerCase 用于将就转换成小写
  8. 实例方法 indexOf 检测是否包含某字符
  9. 实例方法 endsWith 检测是否以某字符结尾
  10. 实例方法 replace 用于替换字符串,支持正则匹配
  11. 实例方法 match 用于查找字符串,支持正则匹配

注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。


Number

Number 是内置的构造函数,用于创建数值。

// 使用构造函数创建数值
  let x = new Number('10')
  let y = new Number(5)
  // 字面量创建数值
  let z = 20

总结:

  1. 推荐使用字面量方式声明数值,而不是 Number 构造函数
  2. 实例方法 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方法,分别放在了motherfather内部。

我们运行代码:

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的属性和方法。

相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
31 0
|
1月前
|
JavaScript
js开发:请解释什么是ES6的类(class),并说明它与传统构造函数的区别。
ES6的类提供了一种更简洁的面向对象编程方式,对比传统的构造函数,具有更好的可读性和可维护性。类使用`class`定义,`constructor`定义构造方法,`extends`实现继承,并可直接定义静态方法。示例展示了如何创建`Person`类、`Student`子类以及它们的方法调用。
22 2
|
2月前
|
JavaScript 前端开发
JavaScript中的正则表达式构造函数和正则表达式字面量
JavaScript中的正则表达式构造函数和正则表达式字面量
|
3月前
|
存储 JavaScript 前端开发
构造函数和原型的结合应用:轻松搞定JS的面向对象编程(三)
构造函数和原型的结合应用:轻松搞定JS的面向对象编程
|
3月前
|
设计模式 JavaScript 前端开发
构造函数和原型的结合应用:轻松搞定JS的面向对象编程(一)
构造函数和原型的结合应用:轻松搞定JS的面向对象编程
|
7月前
js- 面向对象进阶
Object.defineProperty等面向对象的信息
|
3月前
|
存储 JavaScript 前端开发
构造函数和原型的结合应用:轻松搞定JS的面向对象编程(二)
构造函数和原型的结合应用:轻松搞定JS的面向对象编程
|
1月前
|
JavaScript
|
1月前
|
JavaScript 前端开发
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
js继承的超详细讲解:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、class继承
55 0
|
1月前
|
JavaScript 前端开发
探索JavaScript中的构造函数,巩固你的JavaScript基础
探索JavaScript中的构造函数,巩固你的JavaScript基础