toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
22 stars 1 forks source link

细读 JS | 原型详解 #212

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

原文出自 ULIVZ

在此之前,关于原型的认知都是零零散散,没有系统整理过。

引入:普通对象与函数对象

在 JavaScript 中,一直有一种说法,万物皆对象。

事实上,在 JavaScript 中,对象也是有区别的,我们可以将其划分为「普通对象」和「函数对象」。

ObjectFunction 便是 JavaScript 自带的两个典型的「函数对象」。而函数对象就是一个纯函数,所谓的函数对象,其实就是使用 JavaScript 在模拟类。

那么,什么是普通对象,什么又是函数对象呢?

先创建三个 FunctionObject 的实例。

function fn1() {};
var fn2 = function() {};
var fn3 = new Function('getName', 'console.log("Frankie")');

var obj1 = {};
var obj2 = new Object();
var obj3 = new fn1();

打印以下结果,可以得到:

console.log(typeof Object);     // function
console.log(typeof Function);   // function
console.log(typeof obj1);       // object
console.log(typeof obj2);       // object
console.log(typeof obj3);       // object
console.log(typeof fn1);        // function
console.log(typeof fn2);        // function
console.log(typeof fn3);        // function

在上述的例子中,obj1obj2obj3普通对象(均为 Object 的实例),而 fn1fn2fn3函数对象(均是 Function 的实例)。

如何区分呢?记住这句话就行了:

所有 Function 的实例都是函数对象,而其他的都是普通对象。

说到这里,细心的同学会发表一个疑问。文中开头,我们提到 ObjectFunction 均是 函数对象,而这里又说:所有的 Function 的实例都是 函数对象,难道 Function 也是 Function 的实例吗?(先留下疑问)

从图中可以看出,对象本身的实现还是要依靠构造函数,那 原型链 到底是用来干嘛的呢?

众所周知,作为一门面向对象的语言,必定具有以下特征:

原型链最大的目的就是为了实现继承。

进阶:prototype 与 __proto__

原型链究竟是如何实现继承的呢?

首先,我们要引入介绍两兄弟:prototype__proto__,这是在 JavaScript 中无处不在的两个变量,然而,这两个变量并不是在所有的对象上都存在。

对象类型 prototype __proto__
普通对象(NO)
函数对象(FO)

首先,我们先给出以下结论:

function fn() {};
console.log(typeof fn.__proto__);   // function
console.log(typeof fn.prototype);   // object

const obj = {};
console.log(typeof obj.__proto__);  // function
console.log(typeof obj.prototype);  // undefined,普通对象没有 prototype

也就是说,下面代码成立:

console.log(fn.__proto__ === Function.prototype);   // true
console.log(obj.__proto__ === Object.prototype);    // true

看起来很酷,结论瞬间被证明,感觉是不是很爽,那么问题来了:既然 fn 是一个函数对象,那么 fn.prototype.__proto__ 到底等于什么?

这是我尝试去解决这个问题的过程:

  1. 首先用 typeof 得到 fn.prototype 的类型 "object"
  2. 既然是 "object",那 fn.prototype 岂不是 Object 的实例?我们验证一下:
console.log(fn.prototype.__proto__ === Object.prototype);  // true

接下来,如果要你快速地写出,在创建一个函数时,JavaScript 对该函数原型的初始化代码,你是不是也能快速地写出:

// 实际代码
function fn() {};

// JavaScript 自动执行
fn.prototype = {
  constructor: fn;
  __proto__: Object.prototype
}

fn.__proto__ = Function.prototype;

到这里,你是否有一丝恍然大悟的感觉?

此外,因为普通对象就是通过函数对象实例化(new)得到的,而一个实例不可能再次进行实例化,也就不会让另一个对象的 __proto__ 指向它的 prototype,隐藏本节一开始提到的「普通对象没有 prototype 属性」的结论似乎非常好理解了。从上述的分析,我们还可以看出,fn.prototype 就是一个普通的对象,它也不存在 prototype 属性。

再回顾下上一节,我们还遗留了一个疑问:难道 Function 也是 Function 的实例?

是时候去掉「应该」让它成立了:

console.log(Function.__proto__ === Function.prototype);    // true

重点:原型链

上一节,我们详解了 prototype__proto__。实际上,这两兄弟主要就是为了构造原型链而存在的。

先上一段代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}   // 1️⃣

Person.prototype.getName = function() {
  return this.name;
};  // 2️⃣

Person.prototype.getAge = function() {
  return this.age;
};  // 3️⃣

var person = new Person("Frankie", 20); // 4️⃣

console.log(person);  // 5️⃣
console.log(person.getName());  // 6️⃣

// 采用 ES6 更优雅的写法?哈哈
// Object.assign(Person.prototype, {
//  getName() {
//    return this.name;
//  },
//  getAge() {
//    return this.age;
//  },
// })

解析一下执行细节:

  1. 执行 1️⃣,创建一个构造函数 Person,要注意前面已经提到,此时 Person.prototype 已经被自动创建,它包含 constructor__proto__ 这两个属性;
  2. 执行 2️⃣,给对象 Person.prototype 增加一个方法 getName()
  3. 执行 3️⃣,给对象 Person.prototype 增加一个方法 getAge()
  4. 执行 4️⃣,右构造函数 Person 创建一个 person 实例,值得注意的是,一个构造函数在实例化时,一定会自动执行该构造函数。
  5. 在浏览器得到 5️⃣ 的输出,即 person 应该是:
{
  name: 'Frankie',
  age: 20,
  __proto__: Object    // 实际上就是 Person.prototype
}

结合上一节的经验,以下等式成立:

console.log(person.__proto__ === Person.prototype);  // true
  1. 执行 6️⃣ 的时候,由于在 person 中找不到 getName()getAge() 这两个方法,就会继续朝着原型链上查找,也就是通过 __proto__ 向上查找,于是很快在 person.__proto__ 中,即 Person.prototype 中找到了这两个方法,于是停止查找并执行得到结果。

这便是 JavaScript 的原型继承。准确的说,JavaScript 的原型继承是通过 __proto__ 并借助 prototype 来实现的。

于是,我们可以作以下总结:

为了检验你对上述的理解,请分析下面两个问题:

  1. 以下代码输出结果是什么?
console.log(person.__proto__ === Function.prototype);    // false
  1. Person.__proto__Person.prototype.__proto__ 分别指向何处?
console.log(Person.__proto__ === Function.prototype);            // true
console.log(Person.prototype.__proto__ === Object.prototype);    // true

// 分析:
// 1. 前面已经提到过,在 JavaScript 中万物皆对象。Person 很明显是 Function 的实例,
// 因此,Person.__proto__ 指向 Function.prototype。

// 2. 因为 Person.prototype 是一个普通对象,因此Person.prototype.__proto__ 指向 Object.prototype。

// 3. 为了验证 Person.__proto__ 所在的原型链中没有 Object,
// 以及 Person.prototype.__proto__ 所在的原型链中没有 Function, 结合以下语句验证:
console.log(Person.__proto__ === Object.prototype);             // false
console.log(Person.prototype.__proto__ == Function.prototype);  // false

终极:原型链图

上一节,我们实际上还遗留了一个疑问:原型链如果一个搜索下去,如果找不到,那何时停止呢?也就是说,原型链的尽头是哪里?

我们可以快速地利用下面的代码验证:

function Person() {};
var person = new Person();
console.log(person.name);  // undefined

很显然,上述输出 undefined。下面简述查找过程:

person                // 是一个对象,可以继续
person['name']           // 不存在,继续查找 
person.__proto__            // 是一个对象,可以继续
person.__proto__['name']        // 不存在,继续查找
person.__proto__.__proto__          // 是一个对象,可以继续
person.__proto__.__proto__['name']     // 不存在, 继续查找
person.__proto__.__proto__.__proto__       // null !!!! 停止查找,返回 undefined

原来路的尽头是一场空。

最后,再回过头来看看上一节的那演示代码:

function Person(name, age) {
  this.name = name;
  this.age = age;
}   // 1️⃣

Person.prototype.getName = function() {
  return this.name;
};  // 2️⃣

Person.prototype.getAge = function() {
  return this.age;
};  // 3️⃣

var person = new Person("Frankie", 20); // 4️⃣

console.log(person);  // 5️⃣
console.log(person.getName(), person.getAge());  // 6️⃣

我们来画一个原型链图,或者说,将其整个原型链图画出来?请看下图:

画完这张图,基本上之前所有的疑问都可以解答了。

与其说万物皆对象,万物皆空似乎更加形象。

调料:constructor

前面已经有所提及,但只有原型对象才具有 constructor 这个属性,constructor 用来指向引用它的函数对象。

console.log(Person.prototype.constructor === Person);    // true
console.log(Person.prototype.constructor.prototype.constructor === Person);    // true

这是一种循环引用。当然你也可以在上一节的原型链图中画上去,这里就不赘述了。

补充 JavaScript中的 六大内置(函数)对象的原型继承

通过前文的论述,结合相应的代码验证,整理出以下原型链图:

由此可见,我们更加强化了这两个关掉:

  1. 任何内置对象(类)本身的 __proto__ 都指向 Function 的原型对象;
  2. 除了 Object 的原型对象的__proto__ 指向 null,其他所有的内置函数对象的原型对象的 __proto__ 都指向 Object
    console.log(Object.prototype.__proto__ === null);    // true

总结

来几个总结:

最后抛出一个问题:

如何用 JavaScript 实现类的继承?

请接着下一篇 深入 JavaScript 继承原理