从Object.prototype到 null 的距离

2,138 阅读11分钟

构造函数&&实例

JavaScript 是一门基于原型的语言,它没有创建类,对象被创建是基于引用的,JavaScript 也是一种动态编程语言,这意味着可以在实例化后轻松地在对象中添加或删除属性。

在 Js 中没有类的概念,在 Js 中所存在的是构造函数代替模拟类的存在,对象由某个构造函数构造出来,我们称之为这个对象是 构造函数的实例, 即使是使用 var 声明的一个对象,我们也可以看到他的 __proto__ 中的 constructor 也是指向的 Object

prototype && __proto__

我们已知函数通过 new 关键字调用就是所谓的构造函数,得到一个实例。在这个构造函数,或者说 每个函数 上面,都会有一个 prototype 属性,这个 prototype 属性 指向了一个对象,暂且不管这个对象是什么,只需要知道 函数 有一个 prototype 属性,这个属性可以通过 点的方式访问到。

在构造函数所构造的实例上,或者说 Js 中的每一个对象上都会有一个 __proto__ 属性(这个属性虽然可以看到,也可以访问到,但并不建议直接使用 修改 对象的原型;详见)

实例的 __proto__ 也指向了一个对象,实例的 __proto__ 和构造函数的 prototype 指向的是同一个对象,我们称这个对象为 原型对象

例:

    function Fn() {
        this.name = 'Zyc'
    }

    var f = new Fn()

    console.log(Fn.prototype)  // 一个 Object
    console.log(f.__proto__)   // 一个 Object
    console.log('Fn & f', Fn.prototype === f.__proto__)  // true

原型是什么

从位置上来说,确实如上所述,那么 究竟什么是所谓的 原型 呢,我们需要再从 构造函数出发:

函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是 上述 实例 f 的原型。 而所谓的原型就是:每一个 JavaScript 对象(null 除外) 在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从 原型 "继承" 属性

constructor

在每个 原型对象 中,都会有一个 constructor 属性,这个属性指向其关联的构造函数

    function Person() {

    }

    var person1 = new Person()

    console.log('Person && person1', Person.prototype === person1.__proto__) // true
    console.log(Person.prototype.constructor === Person) // true

为什么要将构造函数的方法定义在原型 prototype 上面

其实在构造函数内部直接定义方法也是可以的,问题在于,每个构造函数都可能会调用多次去构建实例,如果把方法定义在 构造函数内部,那么在每次调用的时候,都会重新创建这个函数,伴随着函数的创建,会在内存中开辟一块空间存储这个函数,但是每个实例用到的方法内部代码完全一致,就造成了资源浪费。

如果是将方法定义在函数的 原型 上面,所有的实例都能够访问这同一个方法,不会造成资源浪费,代码更高效

还有一种解决方案,就是在构造函数外部预先定义好一个函数,在构造函数内部引用这个函数,那么所有的实例将拥有同一个函数的地址,也可以避免资源浪费

Ps:一个核心观念就是,构造函数会被调用多次创建实例,每一次调用就会伴随着 函数内部代码的执行,如果内部有函数的声明,将会不断的开辟内存空间,就会资源浪费,如果有方法实现公用一个内存地址,那么就可以避免掉性能浪费的问题

实例 && 原型

当我们在获取一个对象的属性时,会优先在对象内部查找这个属性,当在对象内部没有找到这个属性时,就会去对象的原型上面去找,如果在实例的原型上面也没有找到,就会去原型的原型上面去找。

    function Person() {

    }

    Person.prototype['hobby'] = ['唱', '跳', 'Rap']

    var person1 = new Person() // 此时 Person 的实例 person1 中并没有 hobby 这个属性
    console.log(person1.hobby); // ['唱', '跳', 'Rap']

    person1['hobby'] = ['看书', '学习', '运动'] // 给 person1 实例添加了 hobby 属性
    console.log(person1.hobby); // ['唱', '跳', 'Rap']

原型的原型

已知 原型对象 也是一个对象,其实原型对象就是通过 Object 构造函数生成的,也就是说,一个实例的原型对象Object 的实例, 那么继而可以得出 原型对象__proto__Object 构造函数prototype 指向同一个地址,这个地址也就是 Object原型

    Object.prototype['address'] = 'NJ'

    function Person() {

    }

    Person.prototype['hobby'] = ['唱', '跳', 'Rap']

    var person1 = new Person()
    console.log(person1.hobby); // ['唱', '跳', 'Rap']

    person1['hobby'] = ['看书', '学习', '运动']
    console.log('hobby:', person1.hobby); // ['唱', '跳', 'Rap']

    // 在 Person 的 原型 -> 原型 上定义的 address
    console.log('address:', person1.address); // NJ

    // 未在任何地方定义的属性:
    console.log('age:', person1.age); // undefined

引上述:对象属性的查找规则,我们 既没有在 Person 的 prototype 上定义 address 也没有在 实例内部定义 address,而是在 Object 的 prototype 上定义的 address,在打印 person1.address 时的确打印了出来数据。如果未在任何地方定义的数据 最终获取 为 undefined

:对象查找属性规则 = 对象内部 > 原型 > 原型 > 原型 > undefined

原型链

截止到上面,应该可以体会到一个如同链条的存在,所谓的原型链,就是 原型 => 原型 => 原型 原型串联起来的链条,而在一个对象查找一个属性时,就会在原型链上逐步往上查找,要么找到,要么没找到也就是 undefined

Ps:


    function Person() {

    }

    var person2 = new Person()
    console.log(person2 instanceof Person); // true
    console.log(person2 instanceof Object); // true

如上所述:JavaScript 判断一个对象是否 instanceof 某个函数的依据,即对象 person2原型链 上有没有一个 __proto__ 是这个函数的 prototype, 如果有,那么 person 就是这个函数的 instance。由于一般所有的原型链最终都会指向顶端的 Object.prototype,所以它们都是 Object 的 instance

一句话理解 instanceof 的运算规则为: instanceof 检测左侧的 __proto__ 原型链上,是否存在右侧的 prototype 原型。

原型链的终点

首先 原型链 必须也然有终点,如果没有终点在查找对象的属性时会无限查找下去 其次 原型链 的终点并不是 Object.prototype; 亦可以换种说法,原型链的终点就是 Object.prototype 而这个 Object.prototype 是一个特殊的 Object 它特殊在 它的 __proto__ 指向 null。而我个人更偏向于考虑原型链 的终点为 null

首先需要明确一点,原型链是指 对象 的原型链,所以原型链上的所有节点都是 对象, 不能是字符串、数字、布尔等原始类型。 另外规范要求原型链必须是 有限长度 的(从任一节点出发,经过有限的步骤后必须到达一个终点,显然也不能有环) 那么应该用什么对象作为终点呢? 很显然应该用一个特殊的对象。 Object.prototype 确实是个特殊的对象,我们先假设用它做终点,那么考虑一下,当取它的原型时应该怎么办? 即:Object.prototype.__proto__ 应该返回什么? 当然 JavaScript 已经给了我们答案,先不考虑已知的答案 取一个对象的属性时,可能发生三种情况:

  1. 如果属性值存在,那么返回属性的值
  2. 如果属性不存在,那么返回 undefined
  3. 不管属性存在与否,有可能抛出异常

综上所述, 我们已经假设 Object.prototype 是终点了,哪里还能获取属性,所以排除掉 1 。另外抛出异常也不可取,3 排除。 情况 2,它不存在 原型属性了,返回 undefined 呢? 也不好,因为 undefined 一种解释是原型不存在,但是也相当于原型就是 undefined。这样,在原型链上就会存在一个非对象的值。

所以,最佳选择就是 null。一方面,你没法访问 null 的属性,所以起到了终止原型链的作用;另一方面,null 在某种意义上也是一种对象,即空对象(JavaScript 中之所以存在 null, 就是为了表示空对象的)。这样一来,就不会违反 “原型链 上只有对象”的约定。

所以,“原型链的终点是 null” 虽然不是必须不可的,但确实最合理的。

Function

JavaScript 里任何东西都是对象,包括函数,可以称为函数对象。所以 Foo 也是对象,既然是对象,必然有 __proto__ 属性,那么函数对象 Foo__proto__ 又指向了哪里,又是谁的 instance ?

JavaScript 里定义了一个特殊的函数叫 Function,可以称作所有函数的爸爸,所有的函数都是它的实例,因此你可以认为,定义 Foo 的时候发生了这样的事情:

var Foo = new Function(args, function_body);

于是:

    function Foo() {

    }

    console.log('Function && Foo:', Function.prototype === Foo.__proto__); // true
    console.log(Foo instanceof Function); // true

函数 Foo 由特殊的构造函数 Function 构建,所以 Function.prototypeFoo.__proto__ 指向同一个对象,也就是函数的 原型

注意这里的 Function.prototype,这也是 JavaScript 里一个特殊的对象,Chrome 的 console 里要是输入 Function.prototype,根本什么也打印不出来,什么 native code,就是说它是 内部实现 的。

走到这一步,Function.prototype 并没有走到头,我们继续探索 Function.prototype.__proto__, 可以发现 ··TMD·· 竟然是 Object.prototype

于是:

    Object.prototype['fooName'] = 'HAHAHA'

    function Foo() {

    }

    console.log('Function 的原型的原型:', Function.prototype.__proto__ === Object.prototype); // true
    console.log(Foo.fooName); // HAHAHA

那么问题来了,Function 自己呢? 它其实也是个函数,也是个对象,它的 __proto__ 指向谁? 答案是它自己的 prototype

于是:

    function Foo() {

    }

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

So:所有的函数都是 Functioninstance, Function自己也是它自己的实例,不过后者严格来说并不准确,Function 并不是它自己创造自己的,而应该看作 JavaScript 里原生的一个函数对象,只不过它的 __proto__ 指向了它自己的 prototype 而已。

Function && Object

我们已知一般任何对象都是 Objectinstance, 因为原型链的顶端都指向了 Object.prototype。 那么 Object 本身是什么? Object 也是个函数,而任何函数都是 Function 的实例对象,比如 Array, String,当然 Object 也包括在内,它也是 Function 的实例,于是:

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

    console.log(Object instanceof Function) // true

    console.log(Function instanceof Object) // true

    console.log(Object instanceof Object) // true

    console.log(Function instanceof Function) // true

WDNMD ??

结合上文中所知的解读上述代码:

已知 Object 是所有对象的构造函数,正因为它是一个函数,所以它是 Function 的实例,故 Object instanceof Function

已知 Function.__proto__ 指向 Function.prototype, 指向同一个 原型 这个原型是一个对象且并不是终点,继续获取 Function.prototype.__proto__ 指向了 Object.prototype, 故 Function 的原型链的顶端亦是 Object.prototype,而 instance 的查找规则就是原型链上是否有一个 原型 是某个构造函数的 prototype, 所以 Function instanceof Object 成立

已知 Object.__proto__ === Function.prototypeFunction.prototype.__proto__ === Object.prototype, 故:Object.__proto__ => Function.prototype.__proto__ => Object.prototype 故:Object instance Object 成立

已知:Function.__proto__ === Function.prototype, 故:Function instanceof Function 成立

那么问题来了,FunctionObject 到底谁先谁后,谁主谁次?于是乎,就有了一个 JavaScript 里经常说到的蛋鸡问题:

Object instanceof Function === true > Function instanceof Object === true

借鉴网上的理解:

  • 首先没鸡没蛋,先有一个特殊对象 root_prototype,它是上帝。

  • 接下来应该是先有 Function,并且定义它的 prototype 和proto,都连上了 root_prototype。

  • 然后才有了 Object,它是 Function 的 instance,继承了 Function。这时候 Object 仍然只是个普通的函数。

  • 然后规定 Object.prototype = root_prototype,这时候 Object 才开始显得特殊,成为了原型链的顶端,否则它和其它函数根本没什么区别。

  • 于是所有的东西,包括 Function,都成了 Object 的 instance 了。

这里要强调 Object 和其它函数的不同之处。Object 之所以特殊,就是因为 Object 的 prototype 被设定为了 root_prototype,仅此而已;而其它函数例如 foo,它的 prototype 只是一个普通的对象,这个对象的proto默认情况下指向 root_prototype。至于为什么这样设定,为什么 Object 会特殊化,大概只是因为 Object 这个名字起得好,而 foo,bar 没那么特殊。所以说白了 Object 函数只是一个盛放 root_prototype 的容器而已,从而使它晋升为一个特殊的函数。

另外值得注意的是,obj instanceof function 并不意味着 obj 就是这个 function 创建出来的,只不过是 obj 的原型链上有 function.prototype 而已。

所以所谓的 Object instanceof Function 和 Function instanceof Object 的蛋鸡问题,前者应该来说是自然而然、不容置疑的,可以认为 Object 函数是 Function 创造出来的;而后者说白了只是因为强行规定了 Object 函数的特殊性,而导致的一个推论,而 Function 并非是 Object 创建的。

一些借鉴来源

讶羽的博客 - JS 深入系列 - 从原型到原型链 - github 地址

JavaScript 原型链以及 Object,Function 之间的关系

为什么原型链的终点是 null,而不是 Object.prototype?

JavaScript 自定义构造函数存在的问题(为什么要使用原型)

一张图看懂 Function 和 Object 的关系及简述 instanceof 运算符

JavaScript 世界万物诞生记