原型与原型链以及 Object 与 Function

构造函数与实例

构造函数是一种特殊的函数,用于创建新的对象实例,当我们使用 new 运算符调用一个函数时,它就成为了构造函数,因为它被用来构造一个新的对象实例。

// 构造函数
function Person(name) {
  this.name = name
}

// 对象实例
const person = new Person('maomao')
console.log(person) // Person {name: 'maomao'}

构造函数的特征

  • 函数名通常以大写字母开头,以便将其与普通函数区分开来(本质上没有任何区别)
  • 使用 new 运算符生成实例的函数就是构造函数
  • 直接调用的函数就是普通函数
  • 构造函数中通常使用 this 关键字来指代将要创建的对象实例
  • 构造函数中通常使用属性或方法来定义新对象实例的状态和行为

原型的概念

在 JavaScript 中,原型是一个非常重要的概念,因为它是 JavaScript 实现继承和对象属性共享的机制。

每个 JavaScript 对象在创建时都会关联另一个对象,这个关联的对象就叫做原型(prototype)。当我们尝试访问一个对象的属性时,如果这个对象本身没有这个属性那么 JavaScript 会自动去对象的原型中查找。

每一个构造函数都有一个 prototype 属性,这个属性指向一个对象,这个对象具有一个 constructor 属性,指回原构造函数。使用这个构造函数通过 new 关键字创建的所有实例都将共享这个 prototype 对象。

原型的示例

例如:

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

Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name + ".");
};

var person1 = new Person("Alice");
var person2 = new Person("Bob");

person1.sayHello(); // "Hello, my name is Alice."
person2.sayHello(); // "Hello, my name is Bob."

在上面这个例子中,sayHello 方法是定义在 Person.prototype 上的,所以 person1person2 这两个实例都可以访问到这个方法。

另外值得了解的是,JavaScript 中的对象有一个特殊的 __proto__ 属性(现代浏览器中被标准化为 Object.getPrototypeOf() 方法),它指向该对象的原型。这意味着 person1.__proto__ === Person.prototype

总的来说,原型是 JavaScript 中实现对象间的属性继承的一种机制。每个对象都有一个原型,如果一个对象本身没有某个属性或方法,它会沿着原型链向上查找,直到找到这个属性或者到达原型链的顶端(Object.prototype),如果仍未找到则返回undefined

Constructor

在 JavaScript 中,每个函数都有一个 prototype 属性,它是一个对象。这个 prototype 对象包含了一个特殊的属性 constructor,它指回该函数本身。当这个函数被作为构造函数使用(即通过 new 关键字调用)时,生成的对象实例内部的 __proto__ 会指向构造函数的 prototype 对象。

下面是一个说明 constructor 属性在原型上的作用的例子:

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

Person.prototype.greet = function() {
  console.log('Hello, my name is ' + this.name);
};

var alice = new Person('Alice');
console.log(alice.constructor === Person); // 输出: true
alice.greet(); // 输出: Hello, my name is Alice

// 此时,alice 对象的原型 (__proto__) 中包含一个 constructor 属性,该属性指向 Person 构造函数。

在这个例子中,alice.constructor 指向创建 alice 对象的构造函数 Person。这是因为 alice 的内部 __proto__ 链接到了 Person.prototype,而 Person.prototype.constructor 链接到了 Person 函数。

因此,原型对象上的 constructor 属性提供了一种找回对象实例原始构造函数的方式。这在识别对象类型时很有用,尤其是在处理原型继承和构造函数可能被继承或覆盖的场景下还原回原始的构造函数。

要注意的是有时为了满足特定的设计需求,开发者可能会覆盖原型上的 constructor 属性,这可能导致 constructor 属性不再指向原本的构造函数,因此有时需要手动修正它。

设置原型的几种方法

使用 new 关键字和构造函数(推荐)

当你通过构造函数创建一个新对象时,新对象的__proto__(即[[Prototype]])会被自动设置为该构造函数的prototype属性。

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

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

var person = new Person('Alice');
person.sayHello(); // 输出:Hello, my name is Alice

Object.create() 方法(推荐)

这个方法可以创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

var proto = {
  sayHello: function() {
    console.log('Hello, my name is ' + this.name);
  }
};

var person = Object.create(proto);
person.name = 'Bob';
person.sayHello(); // 输出:Hello, my name is Bob

Object.setPrototypeOf() 方法

ECMAScript 2015 (ES6) 引入了这个方法,可以动态地改变对象的[[Prototype]]

var proto = {
  sayHello: function() {
    console.log('Hello, my name is ' + this.name);
  }
};

var person = {
  name: 'Charlie'
};

Object.setPrototypeOf(person, proto);
person.sayHello(); // 输出:Hello, my name is Charlie

__proto__ 属性

虽然 __proto__ 是历史遗留属性,现代JavaScript开发中不推荐使用,但是它可以被用来直接设置或读取一个对象的原型。请注意,__proto__ 不是所有JavaScript环境的标准属性,它在EcmaScript 6中被标准化,但建议只在兼容性代码中使用。

var proto = {
  sayHello: function() {
    console.log('Hello, my name is ' + this.name);
  }
};

var person = {
    name: 'Dave'
};

person.__proto__ = proto;
person.sayHello(); // 输出:Hello, my name is Dave

在实践中,一般推荐使用 Object.create() 方法和构造函数方式来设置对象原型,因为它们更加清晰并且是标准化的做法。Object.setPrototypeOf() 可以在需要动态改变原型时使用,但是通常应当谨慎使用它,因为它会导致性能问题。避免使用 __proto__,因为它是一个已经被废弃的即将被移除的特性。

原型链的概念

在JavaScript中,原型链(Prototype Chain)是一个允许对象继承另一个对象属性和方法的机制。JavaScript是基于原型的语言,这意味着每个对象都有一个原型对象,并且可以从中继承属性和方法。原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。

原型链的工作原理

这里是原型链的工作原理的一个基本概述:

  1. 每个JavaScript对象(除了null)都有一个属性,它链接到另一个对象,这个链接到的对象被称为它的”原型”。
  2. 当你试图访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,解释器会去它的原型对象上查找。
  3. 如果原型对象上也没有找到,解释器会继续沿着这个原型链向上查找,一直查到原型链的尽头——Object.prototype
  4. 如果在原型链的任何对象上都没有找到所需的属性或方法,操作将会失败(例如,如果你在试图访问一个未定义的属性,它会返回undefined)。

原型链的关键点

  • 对象的原型可以通过__proto__属性(现在已经被弃用,并不推荐在生产环境中使用)来访问,或者使用Object.getPrototypeOf()方法来获取。
  • 函数对象具有一个特殊的属性prototype,当你使用new操作符来创建一个新对象时,新对象的原型就会被设置到构造函数的prototype属性所指向的对象上。
  • Object.prototype位于原型链的最顶端,它没有自己的原型,Object.prototype.__proto__将是null

原型链的示例

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

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

var alice = new Person('Alice');
alice.sayHello(); // 输出: Hello, my name is Alice
console.log(alice.__proto__ === Person.prototype); // 输出: true
console.log(Person.prototype.__proto__ === Object.prototype); // 输出: true
console.log(Object.prototype.__proto__); // 输出: null

// 即使name属性是在alice实例上定义的,sayHello方法却是从原型链上查找到的

在这个例子中,alice.__proto__指向Person.prototype,因为alice对象是使用构造函数Person创建的。同样,Person.prototype.__proto__指向Object.prototype,因为所有自定义对象默认都继承自Object

Object 与 Function

实际的现象

a 是 b 的实例即 a instanceof b 为 true,默认判断条件就是 b.prototype 在 a 的原型链上。

Function instanceof Object   // true
Object instanceof Function   // true

Function instanceof Function // true
Function.__proto__.__proto__ === Object.prototype // true
Object.__proto__ === Function.prototype           // true

Function.__proto__ === Function.prototype         // true

推导关系:

Function.__proto__.__proto__ === Object.prototype  /* => */  Function instanceof Object
Object.__proto__ === Function.prototype            /* => */  Object instanceof Function

Function.__proto__ === Function.prototype          /* => */  Function instanceof Function

Object instanceof Function 返回true是因为Object是一个内置的构造函数,而所有的构造函数在JavaScript中都是Function的实例。这意味着Object是通过Function构造出来的,它继承自Function.prototype

Function instanceof Object 也返回true,这是因为在JavaScript中,Function本身也是一个对象。它是Object的一个实例,因此它继承自Object.prototype。实际上,所有的JavaScript对象都是从Object继承下来的,包括内置的构造函数如FunctionArray等。

总结以下关键点:

  • 所有的构造函数,包括Object,都是Function的一个实例。
  • Function本身是一个特殊类型的对象,因此它也是Object的一个实例。

这表明在JavaScript的原型链中,ObjectFunction互为基础。这是JavaScript语言设计中的一部分,允许这种灵活性和动态原型链。在内部,Function.prototype是一个函数(它实际上是自己的一个实例),而这个函数是从Object.prototype继承来的(Function.__proto__.__proto__ === Object.prototype)。

可以这么理解:

  • 所有函数都是Function的实例 (Object作为构造函数也是如此)。
  • 所有对象(包括函数)都是Object的实例。

JavaScript中的继承是基于原型链的,而原型链的终端始终是Object.prototype,这是所有对象的根源。

经典图

Object-&-Function

参考

从探究Function.proto===Function.prototype过程中的一些收获
深入探究 Function & Object 鸡蛋问题

> cd ..