回头重看前端的面向对象知识(二)

171 阅读16分钟

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

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

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

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

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 绑定到这个新创建的对象上。这个函数称为构造函数。

// 当作构造函数使用
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 语法,使得定义类变得更加直观和简洁,但底层仍然基于原型链。

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 本身是一个全局对象,你可以直接在代码中使用它。

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

2. Object 是一个构造函数

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

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 方法。

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 是一个对象实例。

以下代码展示了这一点:

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

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

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

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'属性可以让自己定义的类也有属于自己的标签定义,参考文章

js原型链指向

要深入了解 JavaScript 对象,必须掌握三个关键属性:prototype__proto__constructor

prototype

  • 是函数的独有属性。
  • 每个函数都有一个 prototype 属性,该属性是一个对象,包含由该函数创建的所有实例所共享的属性和方法。
  • 该属性在构造函数调用时会作为新对象的原型。
function Person(name) {
  this.name = name;
}
console.log(Person === Person.prototype.constructor) // true
console.dir(Person)

image.png

proto

  • 是每个对象都有的属性,指向创建该对象的构造函数的 prototype 属性。
  • 它形成了对象的原型链,使得对象可以从其原型继承属性和方法。
  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作,首先检查对象本身是否有对应的属,如果对象没有该属性,则访问对象的 [[Prototype]](即 __proto__)所指向的原型对象,继续查找,直到找到相应的属性或到达原型链的顶层(null)。
function Person(name) {
  this.name = name;
}
const p = new Person
console.log(p)

image.png constructor

  • 是每个对象所具有的属性,指向创建该对象的构造函数。
  • 当创建一个对象时,这个属性被自动设置。
  • 对象具有 [[Prototype]](即 __proto__)属性。当访问一个对象的 constructor 属性时,如果对象本身没有 constructor 属性,那么会沿着原型链查找,直到找到 constructor 属性。
function Person() {}

var PersonPrototype = Person.prototype
console.log(PersonPrototype.constructor === Person) // true

var p = new Person()
console.log(p.__proto__.constructor === Person) // true
// true  因为当查找对象上的某个key 的时候本身没有就会顺着 __proto__ 往上查找因此可以直接其实本质p.__proto__.constructor
console.log(p.constructor === Person) 
console.log(p.constructor.name === Person.name) // true

三者关系

  • 每个函数都有一个 prototype 属性,它包含由该函数创建的所有实例对象共享的属性和方法。
  • 每个对象都有一个 __proto__ 属性,它指向创建该对象的构造函数的 prototype 属性,从而形成对象的原型链。
  • 每个对象都有一个 constructor 属性,指向创建该对象的构造函数。
  • 因为函数也是对象,所以函数也具有 __proto__constructor 属性,并且函数独有 prototype 属性(箭头函数没有自己的 prototype 属性)
  • 大部分 函数数据类型 的值都具备 prototype(原型/显式原型)属性,属性值本身是一个对象「浏览器会默认为其开辟一个堆内存,用来存储实例可调用的公共的属性和方法」,在浏览器默认开辟的这个堆内存中「原型对象」有一个默认的属性 constructor(构造函数/构造器),属性值是当前函数/类本身
function Person(name) {
  this.name = name;
}

// 函数的 prototype 属性
console.log(Person.prototype); // {constructor: Person}
console.log(Person.prototype.constructor); // [Function: Person]

// 实例的 __proto__ 属性
const alice = new Person("Alice");
console.log(alice.__proto__); // {constructor: Person}
console.log(alice.__proto__ === Person.prototype); // true

// 实例的 constructor 属性
console.log(alice.constructor); // [Function: Person]
console.log(alice.constructor === Person); // true
   Person (函数)
     |
     ├── prototype(显式原型)
     ↓
Person.prototype  
     |
     ├── constructor -> Points back to Person
     ↓
实例 (alice)
     |
     ├── __proto__(隐式原型)
     ↓
Person.prototype

不要去做的事 -- 重写原型对象

每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性,但重新赋值一个对象相当于丢失了指向自身constructor属性,而constructor属性还变成当前赋值对象的

function Person() {

}

console.log(Person.prototype)


// 直接赋值一个新的原型对象
Person.prototype = {
	message: "Hello Person",
	info: { name: "哈哈哈", age: 30 },
	running: function() {},
	eating: function() {},
	// constructor: Person
}

// 非要覆盖 可以这么写
Object.defineProperty(Person.prototype, "constructor", {
	enumerable: false,
	configurable: true,
	writable: true,
	value: Person
})

图解 function 和 Object

构造函数 Foo、实例 f1 以及它们与全局对象 Function 的关系。

// 定义构造函数 Foo
function Foo() {}

// 创建实例 f1
var f1 = new Foo();

image.png

图解说明

  1. 每一个构造函数(比如 Foo)都是 Function 的实例。
  2. 每一个实例对象(比如 f1)的 __proto__ 属性指向其构造函数的 prototype 属性。
  3. 每一个构造函数(比如 Foo)本身也是一个对象,所以它的 __proto__ 会指向 Function.prototype
  4. Function 本身也是一个函数,所以 Function__proto__Function.prototype
  5. Function.prototype__proto__Object.prototype,最终所有对象的 __proto__ 指向 null

图解

      (全局对象) Function
            |
            ├── prototype
            |
   Function.prototype
            |
            ⬇
      +-------------------+
      |                   |
  Foo (构造函数)          +----------------------+
      |                   |                      |
      ├── prototype       |                      |
      |                   |                      ⬇
Foo.prototype             f1 (实例)       Function (全局对象)
      |                   |
      └── constructor  ←-┘
            |
            |
            ⬇
          f1.__proto__
            |
            ⬇
      Foo.prototype
            |
            ⟶ Object.prototypenull

代码示例

为了演示上述关系,以下是实际的代码示例及相关注释:

// 定义构造函数 Foo
function Foo() {}

// 创建实例 f1
var f1 = new Foo();

console.log(Foo.__proto__);       // Function.prototype
console.log(Foo.prototype);       // Foo.prototype 是一个对象,包含 constructor 属性
console.log(Foo.prototype.constructor); // Foo

console.log(f1.__proto__);        // Foo.prototype
console.log(f1.constructor);      // Foo

console.log(Function.__proto__);  // Function.prototype
console.log(Function.prototype.constructor); // Function

// 原型链的终点是 Object.prototype
console.log(Foo.__proto__.__proto__);        // Object.prototype
console.log(Function.prototype.__proto__);   // Object.prototype
console.log(Foo.prototype.__proto__);        // Object.prototype
console.log(Object.prototype.__proto__);     // null

详细解释

  1. FooFunction 的一个实例

    • Foo.__proto__ === Function.prototype (true)
    • 这是因为 Foo 作为一个构造函数,实际上是由 Function 构造出来的。
  2. f1 的原型链

    • f1.__proto__ === Foo.prototype (true)
    • 这是因为 f1 是通过 new Foo() 创建的实例,所以它的 __proto__ 属性指向 Foo.prototype
  3. 构造函数 constructor

    • Foo.prototype.constructor === Foo (true)
    • f1.constructor === Foo (true)
    • 这是因为在定义构造函数 Foo 时会自动创建 Foo.prototype 对象,其中包含 constructor 属性,指向 Foo 函数本身。
  4. 最终的原型链

    • Foo.__proto__ 指向 Function.prototype
    • f1.__proto__ 指向 Foo.prototype
    • Function.__proto__ 指向 Function.prototype
    • Function.prototype.__proto__ 最终指向 Object.prototype
    • Object.prototype.__proto__null,表示该原型链的终点。

其他案例

function DoSomething(){}
console.log( DoSomething.prototype );

DoSomething.prototype.name = 'wang'
// {
//     name: "wang",  ------------> name 属性是DoSomething自己的所以不是从他的copy原型来的,因此不再__proto__
//     constructor: ƒ DoSomething(),
//     __proto__: {    -----------------> 这里的原型链指向的是Object,这证明最开始说的'Object'就是所有对象的原型'
//         constructor: ƒ Object(),
//         hasOwnProperty: ƒ hasOwnProperty(),
//         isPrototypeOf: ƒ isPrototypeOf(),
//         propertyIsEnumerable: ƒ propertyIsEnumerable(),
//         toLocaleString: ƒ toLocaleString(),
//         toString: ƒ toString(),
//         valueOf: ƒ valueOf()
//     }
// }

const doSomething = new DoSomething()
doSomething.age = 17
console.log( doSomething) 
// {
//     age: 17, ---------》 age 属性是doSomething 的因此不再__proto__
//     __proto__: { ---------》doSomething 是从DoSomething克隆来的因此一层原型链指向是DoSomething
//         name: "wang",
//         constructor: ƒ DoSomething(),
//         __proto__: { -----------------》DoSomething 是从Object 来的因此第二层是在Object
//             constructor: ƒ Object(),
//             hasOwnProperty: ƒ hasOwnProperty(),
//             isPrototypeOf: ƒ isPrototypeOf(),
//             propertyIsEnumerable: ƒ propertyIsEnumerable(),
//             toLocaleString: ƒ toLocaleString(),
//             toString: ƒ toString(),
//             valueOf: ƒ valueOf()
//         }
//     }
// }

总结

1. Object 作为一个类(函数),是 Function 类的实例

  • Object instanceof Function => true
  • Object.__proto__ === Function.prototype

2. Function 作为一个类(函数),是 Function 类的实例

  • Function instanceof Function => true
  • Function.__proto__ === Function.prototype

解释:只有这样我们才可保证所有函数都可调用 callapplybind 等方法。

3. 函数也是对象,Function 作为一个普通对象,它是 Object 类的实例

  • Function instanceof Object => true
  • Function.__proto__.__proto__ === Object.prototype

4. Object 作为一个普通对象,它是 Object 类的实例

  • Object instanceof Object => true
  • Object.__proto__.__proto__ === Object.prototype

冷门知识

不具备prototype的函数

  • 箭头函数
  • 基于ES6给对象某个成员赋值函数值的快捷操作

image.png

es5 几种继承


description: js继承

  1. 在 JavaScript 中,是通过遍历原型链的方式,来访问对象的方法和属性,简单的说就是我自身没有我就通过'proto' 找到我的构造函数的'prototype'上,构造函数原型对象没有他就去找构造函数上'proto' 链接的原型对象一直都没找到的情况下,最后找到null 终止
  2. 在原型链上查找属性比较耗时对性能有副作用,这在性能要求苛刻的情况下很重要。另外试图访问不存在的属性时会遍历整个原型链转存失败,建议直接上传图片文件
  3. js属性遮蔽可以理解成就近原则
function A (name) {
    this.name = name
}
A.prototype.name = "原型对象上的name 属性"

const a = new A('w')

console.log(a.name); // w
  • 会找离自身最近属性对应值

hasOwnProperty -- 属性是否是原型对象上,证明打印是自己的而不是原型对象prototype上的,使用hasOwnProperty返回的是true 则使用的是实例对象自己的,false怎相反


 function Person() {}
// 每一个函数都有一个原型属性prototype
// 他们都会指向实例对象因此在Person这个
// 构造函数的prototype加属性即可创建的对象共享
Person.prototype.name = 'wang'

const p1 = new Person()
// 当前的name 到底是p1的还是 Person的
console.log(p1.hasOwnProperty('name'))

const p2 = new Person()
p2.name = 'p2'
// 当前的name 到底是p1的还是 Person的
console.log(p2.hasOwnProperty('name'))

打印结果:
false
true

继承的几种方式

原型链继承

  1. 新实例无法向父类构造函数传参
  2. 所有新实例共享父类实例的属性,导致两个实例使用同一个原型对象,如果属性是引用类型,当一个实例修改属性时,另一个实例也会受到影响。
  3. 通过直接打印对象看不到共享的属性,因为这些属性被挂载在原型链上,调用时如果实例自身没有这些属性,会访问原型链上的属性。
// 第一种 原型链继承
function Parent() {
    this.name = "parent";
    this.play = [1, 2, 3];
}

function Child() {
    this.type = "child";
}

Child.prototype = new Parent();
const child1 = new Child();
const child2 = new Child();

child1.play.push(12345);
console.log(child1.play, child2.play); // [1, 2, 3, 12345] [1, 2, 3, 12345]

构造函数继承

  1. 通过 apply() 和 call() 方法改变函数执行时 this 指向,只有当 new Child 时才执行,给当前 this 调用了一个赋值属性的封装方法。
function Parent(age) {
    this.name = "parent";
    this.age = age;
    this.play = [1, 2, 3];
}

Parent.prototype.getName = function() {
    return this.name;
};

function Child(age) {
    Parent.call(this, age);
    this.type = "child";
}

const child1 = new Child(1);
const child2 = new Child(10);

child1.play.push(12345);
console.log(child1.play, child2.play); // [1, 2, 3, 12345] [1, 2, 3]
// 报错
// console.log(child1.getName());

组合继承

  1. 结合原型链继承和构造函数继承,将二者优点结合,但会导致 Parent 执行两次,一次在创建子类原型时,一次在子类构造函数内部。
  2. 所有子类实例会拥有两份父类的属性,一份在实例自身,另一份在子类原型对象中。访问属性时优先访问实例自身的属性。
function Parent(age) {
    this.name = "parent";
    this.age = age;
    this.play = [1, 2, 3];
}

Parent.prototype.getName = function() {
    return this.name;
};

function Child(age) {
    Parent.call(this, age);
    this.type = "child";
}

// 执行一次 Parent 构造函数
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;

const child1 = new Child(1); // 触发函数执行第二次 Parent.call(this, age)
const child2 = new Child(10);

child1.play.push(12345);
console.log(child1.play, child2.play); // [1, 2, 3, 12345] [1, 2, 3]

原型继承

道格拉斯·克罗克福德提出的一种实现继承的方法,不使用严格意义上的构造函数,通过 Object.create 方法基于已有对象创建新对象。 这种方法并没有使用严格意义上的构造函数。它的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。 先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例从本质上讲,object()对传入其中的对象执行了一次浅复制。 克罗克福德主张的这种原型式继承要求你必须有一个对象可以作为另一个对象的基础

这类思想就是脱离之前构造函数的想法,而是直接给对象做继承,虽然每次伪造了一个 构造函数但对其包装并未暴露,相当于都挂载到原型了导致数据共享问题,无法实现复用,属于自己的属性需要给create 第二个方法传参

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

const parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

const person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

const person5 = Object.create(parent4);
person5.friends.push("lucy");

console.log(person4.name); // tom
console.log(person4.getName() === person4.name); // true
console.log(person5.name); // parent4
console.log(person4.friends); // [ 'p1', 'p2', 'p3', 'jerry', 'lucy' ]
console.log(person5.friends); // [ 'p1', 'p2', 'p3', 'jerry', 'lucy' ]

寄生式继承

寄生式继承是与原型继承相关的一种思想,通过工厂模式添加当前实例的属性和方法。

const parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
        return this.name;
    }
};

function clone(original) {
    const clone = Object.create(original);
    clone.getFriends = function() {
        return this.friends;
    };
    return clone;
}

const person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // [ 'p1', 'p2', 'p3' ]

寄生组合继承

  1. 组合继承的优化方案,避免构造函数执行两次的问题。
  2. 利用 Object.create 挂载原型,减少一次父类实例创建,在子类构造函数使用 call 继承父类属性。
function clone(parent, child) {
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

function Parent6() {
    this.name = "parent6";
    this.play = [1, 2, 3];
}

Parent6.prototype.getName = function() {
    return this.name;
};

function Child6() {
    Parent6.call(this);
    this.friends = "child6";
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function() {
    return this.friends;
};

const person6 = new Child6();
console.log(person6); // Child6 { name: 'parent6', play: [ 1, 2, 3 ], friends: 'child6' }
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child6