JavaScript面向对象
Published on Feb 17, 2023, with 36 view(s) and 0 comment(s)
Ai 摘要:本文系统介绍了JavaScript面向对象编程的核心概念。首先讲解了对象的创建方法(字面量和构造函数),接着详细解析属性描述符(数据属性和存取属性)的作用与配置方式。针对多对象创建需求,分析了工厂模式、构造函数模式及组合模式的优劣。重点阐述了原型机制,包括原型链查找规则、继承实现方案(原型链继承和借用构造函数继承)及其内存表现。最后通过图示展示了完整的原型继承关系,帮助理解JavaScript对象系统的运作原理。

一,JavaScript中对象的理解与创建方法

对象将相关联的数据封装在一起,是对一个事物的抽象概念。比如说把一个人当然对象,这个人可以有姓名、年龄、身高、体重等属性,可以进行工作,吃饭,看电视等动作。因此可以把这个对象描述为:

const person = {
  name: "xiaoming",
  age: 18,
  height: 190,
  eating: function () {},
  working: function () {},
};

JavaScript中创建对象除了用上述的字面量方法(常用)还可以使用object构造函数来创建。

const person = new Object();
person.name = "xiaoming";
person.age = 18;
person.height = 190;
person.eating = function () {};
person.working = function () {};

二,对对象属性的控制——属性描述符

对象创建出来了,那如何对对象中的一些属性进行控制呢?比如说可不可以修改,可不可以遍历等。这就需要用到属性描述符。 属性描述符可以分为数据属性描述符和存取属性描述符。我们可以通过getOwnPropertyDescriptors或者getOwnPropertyDescriptor方法获取属性描述符。

let obj = {
  name: "sun",
};
Object.defineProperty(obj, "height", {
  value: 180,
});
console.log(Object.getOwnPropertyDescriptors(obj));
// {
//   name: {
//     value: "sun",
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   height: {
//     value: 180,
//     writable: false,
//     enumerable: false,
//     configurable: false
//   }
//

这里获取的是对象属性的数据属性描述符。[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;

[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;

[[Writable]]:表示是否可以修改属性的值;

  • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;

  • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;

[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;

  • 默认情况下这个值是undefined;

let obj = {
  name: "sun",
};
Object.defineProperty(obj, "name", {
  enumerable: true,
  configurable: true,
  get: function () {
    console.log("获取name属性值");
  },
  set: function () {
    console.log("设置name属性值");
  },
});
console.log(Object.getOwnPropertyDescriptors(obj));

// {
//   name: {
//     get: [Function: get],
//     set: [Function: set],
//     enumerable: true,
//     configurable: true
//   }
// }

这是获取的对象属性的存取属性描述符。enumerableconfigurable一致,但是没有valuewritable,多了setget[[get]]:获取属性时会执行的函数。默认为undefined[[set]]:设置属性时会执行的函数。默认为undefined

此外,我们还可以通过Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象

let obj = {};
let obj1 = Object.defineProperties(obj, {
  name: {
    writable: true,
    value: "sun",
  },
  age: {
    value: 19,
  },
});
console.log(Object.getOwnPropertyDescriptors(obj1));
// {
//   name: {
//     value: "sun",
//     writable: true,
//     enumerable: false,
//     configurable: false
//   },
//   age: {
//     value: 19,
//     writable: false,
//     enumerable: false,
//     configurable: false
//   }
// }

三,创建多个对象的方案

上述介绍的两种创建对象的方法需要写大量重复的代码。如果需要创建多个对象,可以使用以下方法。

1,工厂模式

function CreatePerson(name, age, height) {
  let obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.height = height;
  return obj;
}
let xiaoming = CreatePerson("xiaoming", 12, 189);
let xiaohong = CreatePerson("xiaohong", 13, 179);
let xiaolv = CreatePerson("xiaolv", 14, 149);

这样通过调用一次方法就返回一个包含多个公有属性的对象。但是酱子作会使创建出来的对象都是一样的类型。

2,构造函数模式

function Person(name, age, height) {
  this.name = name;
  this.age = age;
  this.height = height;
  this.eating = function () {
    console.log(this.name + "在吃东西~");
  };
  this.running = function () {
    console.log(this.name + "在跑步~");
  };
}
let xiaoming = new Person("xiaoming", 12, 189);
let xiaohong = new Person("xiaohong", 13, 179);
let xiaolv = new Person("xiaolv", 14, 149);

这样的话如果需要创建多个汽车对象的话,就可以通过再定义一个Car构造函数然后new Car来实现。啥是构造函数?JavaScript中的构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;new是啥?一个创建对象的操作符。 一个函数被new调用了它会执行以下操作:

  • 在内存中创建一个新的对象(空对象)

  • 这个对象的内部的proto属性会被赋值为这个构造函数的prototype属性

  • 构造函数内部的this会被指向创建出来的新对象

  • 执行构造函数内部的方法体

  • 返回创建出来的新对象

通过构造函数创建多个对象就没有缺点了么?有,它在于我们需要为每个对象的函数去创建一个函数对象实例,比如说小明有eating方法而小红有running方法但是这样创建的话会使小明不仅有eating还有running。并且小明的是小明的,小红的是小红的,没有实现共用。

3,构造函数和原型相组合方式:将多个对象中相同的方法放到构造函数的原型上去。

function Person(name, age, height, address) {
  this.name = name;
  this.age = age;
  this.height = height;
  this.address = address;
}
Person.prototype.eating = function () {
  console.log(this.name + "在吃东西~");
};
Person.prototype.running = function () {
  console.log(this.name + "在跑步~");
};
var p1 = new Person("why", 18, 1.88, "广州市");
var p2 = new Person("kobe", 30, 1.98, "北京市");
p1.eating();
p2.running();

四,对象的原型

JavaScript中每一个对象都有一个prototype属性,该属性指向另一个对象,这个属性有啥用呢?又如何获取呢?作用

  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;

  • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;

  • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

获取方式

  • 通过对象的proto属性获取

  • 通过Object.getPrototypeOf()方法获取

对象有proto属性,函数有prototype属性

let obj = {
  name: "sun",
};
function person() {}
console.log(obj.__proto__); // [Object: null prototype] {}
console.log(person.prototype); // {}

创建对象的内存表现

Description

constructor属性默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象;

五,原型链

从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取

let obj = { name: "why", age: 18 };
obj.__proto__ = {};
obj.__proto__.__proto__ = {};
obj.__proto__.__proto__.__proto__ = { address: "北京市" };
console.log(obj.address);//北京市

那么什么地方是原型链的尽头呢?

let obj = { name: "why", age: 18 };
console.log(obj.__proto__);//[Object: null prototype] {}

[Object: null prototype] {} 即顶层原型,该对象有原型属性,但是它的原型属性已经指向的是null,并且该对象上有很多默认的属性和方法。

六,继承

JavaScript中可以通过原型链来实现继承

//1.定义父类构造函数
function Person() {
  this.name = "sun";
}
//2.父类原型上添加内容
Person.prototype.running = function () {
  console.log(this.name + "running~");
};
//3.定义子类构造函数
function Student() {
  this.sno = 111;
}
// 4.创建父类对象,并且作为子类的原型对象
var p = new Person();
Student.prototype = p;
// 5.在子类原型上添加内容
Student.prototype.studying = function () {
  console.log(this.name + "studying");
};

var s1 = new Student();//通过子类来调用父类中的方法
s1.studying();//"sunstudying"
s1.running(); //"sunrunning~"

这里Person中有running方法,Student中有studying方法,通过让Student的原型指向person实现继承,下面打印说明了stu中确实有来自person中的方法。 但是目前有一个很大的弊端:某些属性其实是保存在p对象上的;

  • 第一,我们通过直接打印对象是看不到这个属性的;

  • 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;

  • 第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);

借用构造函数继承为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象);steal是偷窃、剽窃的意思,但是这里可以翻译成借用; 借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数. 因为函数可以在任意的时刻被调用; 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;

// 父类: 公共属性和方法
function Person(name, age, friends) {
  // this = stu
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  // this.name = name
  // this.age = age
  // this.friends = friends
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

七,原型继承关系

Description