对 JS 原型链的理解

这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!

JS 的类型

js 里有七种原始类型和 Object

原始类型

number 是基于 IEEE 754 标准的双精度 64 位二进制格式的值(-(253 -1) 到 253 -1)。 它并没有为整数给出一种特定的类型。 除了能够表示浮点数外,还有一些带符号的值:+Infinity,-Infinity 和 NaN (非数值,Not-a-Number)。

bigint 可以用任意精度表示整数。 在将 bigint 转换为 boolean 时,它的行为类似于一个 number 。 bigint 不能与 number 互换操作。否则,将抛出 TypeError 。

bigint 和 symbol 都是 es2015 新增的,平时接触得比较少。 symbol 可以简单但不严谨地类比成 C 里面的枚举类型。

基本字符串和字符串对象的区别

请注意区分 JavaScript 字符串对象和基本字符串值。( 对于 Boolean 和Numbers 也同样如此。)

字符串字面量 (通过单引号或双引号定义) 和 直接调用 String 方法(没有通过 new 生成字符串对象实例)的字符串都是基本字符串。

JavaScript 会自动将基本字符串转换为字符串对象,只有将基本字符串转化为字符串对象之后才可以使用字符串对象的方法。

当基本字符串需要调用一个字符串对象才有的方法或者查询值的时候(基本字符串是没有这些方法的), JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或者执行查询。

参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String

Object

Object 是引用类型。 大多数情况下引用类型的赋值都是浅拷贝,例如这样。要完整地复制一个对象,需要深拷贝(这是一个可以单独写一篇文章的主题)。

// 引用数据类型赋值
var a = {name: '张三'};
var b = a;
console.log(a); // {name: "张三"}
console.log(b); // {name: "张三"}

函数是 Object , 数组是 Object , 可以简单但不严谨地理解为,除了原始类型之外都是 Object 。

null 和 undefined

NaN 是一个特殊的值,但在 es2015 里, NaN 是 Number 的一个属性。 NaN 最好只用来判断一个变量是不是数字。

类型检测

typeof 只能检测原始数据类型。 数组,对象这类的全都是返回 object 。 但 null 也是返回 object 。 但函数返回的是 function 。

笔者认为的检测类型最安全的方法是 Object.prototype.toString.call(variable)

对象和函数

对象

对象分类:

  1. 内置对象
    • 由 es 标准中定义的对象,在任何的 es 的环境中都可以使用,例如:Object Function Math Date 等
  2. 宿主对象
    • 由 js 的运行环境提供的对象,例如由浏览器提供的对象:BOM(浏览器对象模型) DOM(文档对象模型)
  3. 自定义对象
    • 由开发者创建的对象

在 js 里每一个对象都有一个原型。 对象的原型也是一个对象。 对象会继承原型的属性和方法。 对象会覆盖原型里同名的属性和方法。 所有对象的原型最终都会指向 Object.prototype 。 Object.prototype 的原型是 null 。 可以通过 __proto__ 属性或 Object.getPrototypeOf() 来访问原型。

函数

函数可以理解成一个变量。 函数的类型是 Object 。 函数名和变量名相同时会提示语法错误。

// 一个函数的声明
function a(){};
// 也可以写成这样
var a = function(){};

函数都有一个名为 prototype 的属性,这个属性的值是一个对象。 这个对象会被称为函数的原型对象。 原型对象会有一个名为 constructor 的属性,这个属性会指向函数本身。

prototype 和 __proto__ 是不一样的。 prototype 只有函数才有, __proto__ 所有对象都有。 函数也有 __proto__ 属性。

所有函数的原型最终都会指向 Function.prototype 。 Function.prototype 的原型是 Object.prototype 。 Object.prototype 的原型是 null 。

Function 的 prototype 和 __proto__ 是相等的

console.log(Object.is(Object.getPrototypeOf(Function), Function.prototype)); // true

new 运算符

new 关键字会进行如下的操作:

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 为步骤1新创建的对象添加属性 __proto__ ,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为 this 的上下文 ;
  4. 如果该函数没有返回对象,则返回 this 。

例子

(function(){
    let a = function(){};
    let b = new a();
    console.log(Object.is(b.__proto__, a.prototype)); // true;
    console.log(Object.is(a, a.prototype.constructor)); // true;
    console.log(Object.is(a.__proto__, Function.prototype)); // true;
    console.log(Object.is(Function.prototype.__proto__, Object.prototype)); // true;
})();

根据上面的例子, a 会被称为构造函数, b 会被称为 a 的实例。 a 本质上只是一个普通的函数,但使用 new 运算符创建新的对象 b 时, a 就可以被称为构造函数。 在一些语境下 a 也会被称为类。

参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new

原型链

需要关注这三个属性

在访问对象的属性时,会先查找自身的属性,如果没有找一级一级地向上查找原型的属性。 试图访问对象不存在的属性时会遍历整个原型链。

@startuml
(null)
(Object.prototype)
(Function.prototype)
(Array.prototype)
(function Function) as funFunction
(function Object) as funObject
(function Array)as funArray
(function Example)as funExample
(Example.prototype)
(exampleObj)
(exampleArr)
(var obj = new Object<U+0028><U+0029>) as obj1
(var fun = new Function<U+0028><U+0029>) as fun1

Object.prototype --> null : "<U+005F>_proto__"
Function.prototype --> Object.prototype : "<U+005F>_proto__"
Example.prototype --> Object.prototype : "<U+005F>_proto__"
Array.prototype --> Object.prototype : "<U+005F>_proto__"
funFunction --> Function.prototype : "<U+005F>_proto__"
funObject --> funFunction : "<U+005F>_proto__"
funArray --> funFunction : "<U+005F>_proto__"
funExample --> funFunction : "<U+005F>_proto__"
exampleObj --> Example.prototype : "<U+005F>_proto__"
exampleArr --> Array.prototype : "<U+005F>_proto__"
obj1 --> Object.prototype : "<U+005F>_proto__"
fun1 --> Function.prototype : "<U+005F>_proto__"

funArray --> Array.prototype : "prototype"
funExample --> Example.prototype : "prototype"
funFunction --> Function.prototype : "prototype"
funObject --> Object.prototype : "prototype"

Array.prototype --> funArray : "constructor"
Example.prototype --> funExample : "constructor"
Function.prototype --> funFunction : "constructor"
Object.prototype --> funObject : "constructor"
@enduml

js 的原型链

这个图里, function Excemple 是自定义的函数。 function Array 是 js 的内置函数。

in 和 hasOwnProperty 的区别

例子

(function(){
    let Person = function() {};
    Person.prototype.lastName = "Deng";
    let person = new Person();
    person.age = 12;
    console.log(person.hasOwnProperty('lastName')); // false
    console.log(person.hasOwnProperty('age')); // true
    console.log('lastName' in person); // true
    console.log('age' in person); // true
})()

getPrototypeOf 和 __proto__

__proto__ 是已经废弃了的属性 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/proto

标准是使用 Object.getPrototypeOf() 方法替代 __proto__

其它

原生原型不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。

不建议重新定义原型,不要重新定义函数的 prototype 属性。

直接通过 console.log 输出的对象数据时,总是和预期的有一点不一样。 可能大概是因为这样,所以 es2015 才会要求使用 Object.getPrototypeOf() 方法获取对象原型而不是通过 __proto__ 属性获取对象原型。

js 是一种函数优先的编程语言,函数可以作为变量、参数、返回值。 js 也是一种面向对象的编程语言,但和 Java C++ 不同的是, js 并没有区分 类 和 对象。 js 只有对象,并使用原型链的方式来实现对象的继承。

class 只是原型链的语法糖。

除了原型链之外 js 还有很多语法细节上的坑,例如 this 闭包 事件循环 IIFE

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Details_of_the_Object_Model

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

https://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html

https://www.liaoxuefeng.com/wiki/1022910821149312/1023021997355072

https://zh.javascript.info/prototype-inheritance