Skip to content

js的面向对象

✍️ w 🕒 2024-06-17 14:53:00(5 days ago) 🔗 A.前端知识整理

JavaScript 中的类与其他面向对象语言(如 Java、C++)不同,其核心依赖于原型链。这让初学者有时会感到困惑,因为在 JavaScript 中,类和方法的定义方式以及它们的行为存在许多独特之处。

JavaScript 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆他

在 JavaScript 中,类是通过原型链实现的。这意味着对象继承自另一个对象,而非直接从类继承。这个概念在 ES6 之前尤其明显,因为那时没有显式的 class 语法。来看一个 ES5 的示例:

javascript
function Person() {
    this.name = 'wang';
}

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

var per = new Person();
console.log(per.name);  // 'wang'
per.sayName();          // 'wang'

在这个示例中,Person 是一个构造函数,通过 new 关键字调用时会创建一个新的实例,并且 sayName 方法定义在 Person.prototype 上,使得所有实例共享这一方法。

ES5 中的类和函数

在 ES5 中,函数可以充当类的角色,但它们在不同使用场景下的行为有所不同。首先来看一个例子:

javascript
function fPerson() {
    this.name = 'wang1';
}

function Person() {
    this.name = 'wang';
}

// 调用 fPerson 作为普通函数
fPerson();
console.log(name); // 'wang1',在非严格模式下,this 绑定到了全局对象

// 调用 Person 作为构造函数
var per = new Person();
console.log(per.name); // 'wang',this 绑定到新创建的实例上

JavaScript 中没有传统编程语言的类的概念。在许多面向对象的编程语言中,类是用于创建对象的蓝图。然而,在 JavaScript 中,所谓的“类”只是因为语法上和其他具有类概念的语言类似而给人的错觉。实质上,JavaScript 是基于原型的。

原型模式不仅是一种设计模式,也是一种编程范式,JavaScript 采用的是原型编程的思想。传统的面向对象编程中,类是创建对象的基础。但在原型编程中,类并不是必须的。从设计角度来看,原型是一种创建对象的模式。在这种模式下,一个对象(原型对象)用于创建具有相同特性的其他对象。对象可以通过克隆(复制)其他对象来创建。一句话总结:在原型编程中,一个对象可以通过克隆另一个对象而存在

对象的继承关系

如果对象 A 是从对象 B 克隆出来的,那么 B 对象就是 A 对象的原型。在 JavaScript 中,Object 是所有对象的顶级原型。因此,可以理解为所有对象都“克隆”了一份 Object 对象。

原型的引用

在 JavaScript 中,克隆一个对象并不是真正复制这个对象的所有内容,而是使新对象 持有对原型对象的引用。这意味着,新对象可以访问原型对象的所有属性和方法。JavaScript 中对象的这种克隆机制实现了高效的内存利用和对象共享。

小结:

  1. JavaScript 中并没有真正的类:所谓的类只是形式上的相似。
  2. 原型模式:本质是通过克隆对象来实现对象创建。
  3. 对象继承:一个对象可以从另一个对象克隆而来,克隆对象持有原型对象的引用。
  4. 原型链:所有对象的顶级原型是 Object,对象通过原型链共享属性和方法。

[[Call]] 和 [[Construct]] 内部方法

出现上面说的函数在不同调用情况所表现的不同现象完全因为 JavaScript 函数有两个重要的内部方法:[[Call]][[Construct]]

  • [[Call]]:函数以普通方式调用时执行。例如:fPerson()
  • [[Construct]]:函数以构造函数方式调用时执行。例如:new Person()

区别与用法

  1. 普通函数调用:当你直接调用一个函数(如 fPerson())时,JavaScript 执行 [[Call]]。在这种情况下,this 会绑定到全局对象(在非严格模式下)或者 undefined(在严格模式下)。

  2. 构造函数调用:当你使用 new 关键字调用一个函数(如 new Person())时,JavaScript 执行 [[Construct]]。这会创建一个新的对象并将 this 绑定到这个新创建的对象上。这个函数称为构造函数。

js
// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"

// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window
window.sayName(); //"Greg"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"

扩展:使用 ES6 类语法

从 ES6 开始,JavaScript 引入了 class 语法,使得定义类变得更加直观和简洁,但底层仍然基于原型链。

javascript
class Person {
    constructor(name) {
        this.name = name;
    }

    sayName() {
        console.log(this.name);
    }
}

let per = new Person('wang');
console.log(per.name);  // 'wang'
per.sayName();          // 'wang'

小结

  • JavaScript 类通过原型链实现:不同于大部分基于类的语言。
  • 函数两种调用方式
    • 普通函数调用([[Call]]):执行函数体,this 绑定取决于调用方式。
    • 构造函数调用([[Construct]]):创建实例并将 this 绑定到实例。

Object 既是一个构造函数也是一个对象

在 JavaScript 中,Object 既是一个构造函数也是一个对象。我们可以通过以下几方面来理解这一点。

1. Object 是一个对象

Object 本身是一个全局对象,你可以直接在代码中使用它。

javascript
console.log(typeof Object); // 'function'

2. Object 是一个构造函数

除了作为对象,Object 也是构造函数,用来创建新的对象实例。

javascript
const obj = new Object();
console.log(typeof obj); // 'object'

类的定义

在 JavaScript 中,“类”的定义是通过一个内部属性 [[class]] 来表示的。这是一个私有属性,普通开发者不能直接访问。

内置类型的 [[class]]

对于内置类型如 NumberStringDate 等,JavaScript 规范为它们指定了 [[class]] 属性以表示其类型。比如:

  • Number[[class]]"Number"
  • String[[class]]"String"
  • Date[[class]]"Date"

访问 [[class]] 属性

JavaScript 提供了唯一访问 [[class]] 属性的方法,那就是通过 Object.prototype.toString 方法。

javascript
const obj = Object();
console.log(typeof obj); // 'object'
console.log(Object.prototype.toString.call(obj)); // '[object Object]'

示例与解释

基本用法与 typeof 运算符

  • typeof Object 返回 'function',说明 Object 是一个函数。
  • typeof obj 返回 'object',说明 obj 是一个对象实例。

以下代码展示了这一点:

javascript
const obj = Object();
console.log(typeof obj); // 'object'
console.log(typeof Object); // 'function'

使用 Object.prototype.toString 访问 [[class]]

Object.prototype.toString 方法可以用来查看对象的 [[class]] 属性。比如:

javascript
function outTypeName(data, type) {
    let typeName = Object.prototype.toString.call(data);
    console.log(typeName);
}

outTypeName(Object); // '[object Function]'
outTypeName(String); // '[object Function]'
outTypeName(Number); // '[object Function]'

上述示例中,ObjectStringNumber 都是内置构造函数,因此它们的 [[class]] 属性被表示为 'Function'。虽然我们平时称它们为内置对象,但是从底层实现来看,它们本质上是内置函数,可以用来创建特定类型的实例。

分析输出结果

  • outTypeName(Object) 的输出是 [object Function]Object 是一个函数,因此返回 [object Function]
  • outTypeName(String)outTypeName(Number) 的输出同理,它们也是内置构造函数。

小结

  1. Object 是一个全局对象,同时也是一个构造函数,用于创建新的对象实例。
  2. 所有内置类型如 NumberStringDate[[class]] 属性可以通过 Object.prototype.toString 方法访问。
  3. Object.prototype.toString.call(data) 是检查数据类型或内部 [[class]] 属性的一种可靠方法。

使用Symbol.toStringTag 创建自己'toString' 返回类型

es6新增'Symbol.toStringTag'属性可以让自己定义的类也有属于自己的标签定义,参考文章

Released under the MIT License.