JavaScript(三):对象

68 阅读7分钟

创建对象的几种方式

对象字面量创建

var person1 = {
    name : "Bob",
    age : 18,
    sayName : function () {
        console.log(this.name);
    }
}

new Object() 创建

var person = new Object();
person.name = "Bob";
person.age = 18;
person.sayName = function () {
    console.log(this.name);
}
person.sayName();

工厂模式创建

function createPerson(name,age) {
    return {
        name : name,
        age : age,
        sayName : function () {
            console.log(this.name);
        }
    }
}

//对象实例化
var person1 = createPerson('Mike',20);

构造函数创建

function Person (name,age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
}
var person1 = new Person('Mike',19);
var person2 = new Person('Bob',21);

构造函数首字母要大写。

new 关键字的用途

  • 创建一个新对象

  • 将函数内部的 this 指向了这个新对象

  • 执行构造函数内部的代码

  • 将新对象作为返回值

构造函数的优点

通过构造函数创建的实例对象,都会有个 constructor(构造函数)属性,实例对象就是通过这个属性来指向构造函数的

function Person (name,age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
}

var person1 = new Person('Mike',19);
var person2 = new Person('Bob',21);

console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true

// 不过用 instanceof 操作符来检测对象类型会更加可靠,因为 constructor 可以手动覆盖
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true

instanceof 原理是检测左侧实例对象的原型链上,是否存在右侧构造函数的 prototype 属性。

创建自定义的构造函数意味着将来可以将他的实例标识为一种特殊类型,这就是构造函数比工厂模式强的地方,因为构造函数生成的实例知道自己从哪里来,通过谁产生的,这很重要。

构造函数的缺点

构造函数最明显的缺点就是,每个方法都要在每个实例上重新创建一遍。

console.log(person1.sayName === person2.sayName); //false

判断各自方法发现他们各自都开辟了一个新的空间,这引起了一种内存的浪费。

这样我们就需要原型对象来帮我们解决这个问题了。

原型对象(Prototype Object)

在 JavaScript 中,每一个函数都有一个特殊的属性叫做 prototype。这个 prototype 属性是一个指针,指向了一个对象,这个对象被称为该函数的原型对象。

const obj = {age: 1}
console.log("obj.prototype:", obj.prototype) // undefined 普通对象没有 prototype 属性

function fn() {
    return 1
}
console.log("fn:", fn)
console.log("fn.prototype:", fn.prototype)

image.png


默认情况下,原型对象仅包含一个名为 constructor 的属性,该属性是一个指向函数本身的指针。

function fn() {
    return 1
}
console.log("prototype:", fn.prototype)
console.log("constructor.:", fn.prototype.constructor)

image.png


虽然普通函数也有 prototype 属性指向它的原型对象,但是我们在谈论原型对象时更多的是从构造函数上谈论,而不是从普通函数上谈论。这是因为普通函数主要用于执行特定的任务或计算,并返回结果。它们通常不直接参与对象的创建和初始化过程,因此与原型对象的关联相对较弱。

下面将以构造函数的角度来说明原型对象。

原型对象可以抽象理解为:连接着构造函数和它的实例对象的一个公共空间,在这个公共空间内创建的对象和属性,会传递给构造函数,而且构造函数创建的实例对象也可以调用这些函数和属性。

function Person (name,age) {
    this.name = name;
    this.age = age;
}

// 原型对象可以添加一些属性和方法
Person.prototype.sayName = function () {
    console.log(this.name);
}}

var person1 = new Person('Mike', 19);
var person2 = new Person('Bob', 21);

console.log(person1.sayName === person2.sayName); // true

// 原型对象的属性和方法只要实例对象可以访问,构造函数不能访问
console.log(Person.sayName) // undefined

image.png

  • 构造函的 prototype 属性指向自己的原型对象

  • 原型对象的 constructor 属性返过来指向构造函数本身(调用方式:构造函数名.prototype.constructor

  • 构造函数通过 new 方法生成实例对象

  • 实例对象的 __proto__ 属性指向自己的原型对象

    • 函数通过 .prototype 找到原型对象,对象通过 .__proto__ 找到原型对象

    • 需要注意的是:日常开发中不会写 实例对象.__proto__.原型对象方法名,而是直接写 实例对象.原型对象方法名 。因为 __proto__ 属性并不是一个标准的属性,是浏览器根据语法自动生成的。

function Person (name,age) {
    this.name = name;
    this.age = age;
}
var person = new Person('Mike', 19);

// 构造函的 prototype 属性指向自己的原型对象
console.log('Person.prototype:', Person.prototype)

// 原型对象的 constructor 属性指向构造函数本身
console.log('Person.prototype.constructor :', Person.prototype.constructor )

// 实例对象的 `__proto__` 属性指向自己的原型对象
console.log('person.__proto__:', person.__proto__)

// 构造函数的原型对象 === 实例对象的原型对象
console.log('Person.prototype === person.__proto__:', Person.prototype === person.__proto__)

image.png

原型对象的作用

  • 共享属性和方法

  • 实现继承

  • 动态扩展对象

  • 减少内存占用

[[Prototype]]__proto__getPrototypeOf

  • [[Prototype]]__proto__ 都是用于指向对象的原型对象,区别是:

    • [[Prototype]] 不可读、不可写,只能看

    • __proto__ 可读、可写,但它并不是一个标准的方法

  • 在现代的 JavaScript 开发中,推荐使用 Object.getPrototypeOf(object)Object.setPrototypeOf(object, prototype) 方法来获取和设置对象的原型。

原型对象使用建议

当为原型对象添加多个属性时:

Person.prototype.type1 = xxx;
Person.prototype.type2 = xxx;
Person.prototype.say = function() {
	...
};

此类写法比较繁琐,可以用如下的写法:

Person.prototype = {
    constructor : Person, // 必须手动把 constructor 属性指向正确的构造函数
    type1: "a",
    type2: "b"
    say: function () {
        ...
    }
}

原型链(Prototype Chain)

原型链查找机制

每个对象(null、undefined 等特殊的对象除外),都有一个【原型对象】(通过 [[Prototype]])。当尝试访问一个对象的属性时,如果该对象本身没有这个属性,那么 JavaScript 就会在该对象的【原型对象】上查找这个属性,如果原型对象上还没有,就会继续在 【原型对象】的【原型对象】上查找(因为原型对象也是一个对象),以此类推,直到找到该属性或者到达原型链的末尾(通常是 Object.prototypenull)。如果都没有,就会返回 undefined。

所以,【原型链】就是由多个原型对象连接起来形成的链式结构

image.png

Object.prototype 中的函数介绍

首先说明:Object 是一个构造函数。Object 的原型对象(即 Object.prototype)有很多方法:

属性/方法说明
constructor指向创建该对象实例的构造函数
toString()返回一个表示该对象的字符串
valueOf()返回指定对象的原始值
hasOwnProperty(propertyName)检查对象自身(不包括原型链)是否具有指定的属性
isPrototypeOf(object)检查该对象是否为另一个对象的原型
propertyIsEnumerable(propertyName)检查指定的属性是否可以被 for...in 循环枚举
toLocaleString()返回一个表示该对象的本地化字符串。它通常与 toString() 返回相同的结果)

静态成员和实例成员

在 ES6 之前,使用构造函数和原型对象来模拟类和继承。在 ES6 及之后的版本中,我们有了 class 关键字,这使得声明类及其静态成员变得更加直观。

静态成员通常使用 static 关键字来定义,它们属于类本身,而不是类的实例。这意味着你可以通过类名直接访问它们,而无需创建类的实例


示例:使用 ES6 的 class 语法:

class MyClass {  
  // 静态成员
  static myStaticMethod() {  
    return '静态成员';  
  }  
  
  // 实例成员
  myInstanceMethod() {  
    return '实例成员';  
  }  
}  
  
// 访问静态成员
console.log(MyClass.myStaticMethod()); // 输出: 静态成员
  
// 访问实例成员报错
console.log(MyClass.myInstanceMethod()); // TypeError: MyClass.myInstanceMethod is not a function

// 正确示例
const myInstance = new MyClass();  // 创建实例对象
console.log(myInstance.myInstanceMethod()); // 输出: 实例成员

在上面的示例中,myStaticMethod 是一个静态成员,它可以通过类名 MyClass 直接访问。而 myInstanceMethod 是一个实例成员,它需要通过类的实例来访问。

  • 静态成员属于类本身,实例成员是属于实例对象的。

  • 静态成员通常用于提供与类本身相关的功能,实例成员则用于描述和操作类的具体实例。


示例:使用构造函数和原型对象:

function Person (name,age) {
    // 实例成员,只能通过实例对象访问
    this.name = name;
    // 实例成员,只能通过实例对象访问
    this.age = age;
}

// 原型对象的方法,只能通过实例对象访问
Person.prototype.sayName = function () {
    console.log(this.name);
}

// 静态属性,只能通过构造函数访问
Person.sex = '男'

// 创建实例对象
var instance = new Person('张三', 19);

console.log("访问实例成员:", instance.name)
console.log("访问原型对象的方法:", instance.sayName)
console.log("访问静态属性:", Person.sex)