Life is too short to waste a second

JavaScript 深入系列 原型

前言

首先强烈安利 冴羽 的博客,我的很多 JavaScript 进阶都是在上面学习的 https://github.com/mqyqingfeng/Blog

虽然 冴羽 大佬已经写的很详细了,不过我还在在这里相当于做笔记或者复习再次对自己有兴趣的部分进行补充

原型

你有没有想过我们常用的类型方法是在哪里定义的呢? 比如

const str = 'string'
str.toString()

让我们来看看着其中发生了什么,首先我们定义了一个 str 常量,然后给它赋值。如果你使用 typeof 查看它的类型,就是 string。JS已经将赋值的一串字符串识别为 String 了。第一行代码其实就将 str 通过 JS 中自带的 String 构造函数进行了初始化。原型又是什么呢?这其实很 java,我们可以将原型理解为一个类,str = new String() ,JS 也允许这样的写法,不过你可能会收到不推荐使用的提示。我们知道 new 一个类的同时,子类也就有了父类的方法。在上述代码中,toString 其实就是 String 原型中的方法。但需要注意的是,JS 中其实并不是像 Java 一样继承父类,而是通过原型链将它们关联起来。

构造函数访问原型 prototype

String 是一个构造函数,我们可以使用 String.prototype 来访问 String 的原型,然后我们来做一点有意思的操作

const str = 'string'
String.prototype.toString = () => 'I changed the prototype toString'
str.toString()

我们通过 String 构造函数的 prototype 访问到了原型,并将它的 toString 方法进行改动,让它返回一段字符串。这时你再调用 str.toString() 得到的将是你修改的字符串,而不是 str 的值了。其实这里应该上一个原型图,但是没时间做了,你可以看看前言推荐的博客中的图片。

对象访问原型 __proto__

上面的例子是通过构造访问的原型,那么对象呢?也就是我们的 str。

const str = 'string'
// 通过 Object.getPrototypeOf
Object.getPrototypeOf(str).toString = () => 'getPrototypeOf'
str.toString()
// 通过 __proto__
str.__proto__.toString = () => '__proto__'
str.toString()
console.log(str.__proto__ === Object.getPrototypeOf(str)) // true
console.log(Object.getPrototypeOf(str) === String.prototype) // true

我们可以通过 Object.getPrototypeOf 和 __proto__ 来访问原型,和构造函数的 prototype 访问到的是一样的。

原型访问构造函数

原型是肯定访问不了单个对象的,但是它能访问到它的构造函数

const str = 'string'
console.log(str.__proto__.constructor === String) // true

原型的原型

原型是否有原型呢?试试不久知道了

// String 原型指向 object
console.log(String.prototype.__proto__) // {}
// Oject 原型指向 null
console.log(Object.prototype.__proto__) // null

原型链

递归查找

前面提到过原型链,简单的说就是递归查找,我们来看看这个例子

const MyConstructor = function () {
  this.a = 1
}

const demo = new MyConstructor()
// demo 是来自 MyConstructor 的对象,MyConstructor 原型并没有定义 hasOwnProperty 方法
// demo 查看自己是否有 hasOwnProperty 方法?没有,查看 MyConstructor 原型,没有
// 查看 MyConstructor 原型的原型也就是 object,有 hasOwnProperty ,调用它
console.log(demo.hasOwnProperty('a'))
// 原型的原型都将指向 object
console.log(MyConstructor.prototype.__proto__) // {}
console.log(String.prototype.__proto__) // {}
console.log(Number.prototype.__proto__) // {}
// object 原型的原型指向 null
console.log(Object.prototype.__proto__) // null

可以看到,JS 是自底而上的查看原型中是否有某个属性,这很像是 DNS 的递归查找,因为一旦底层已经有了某个属性,那么 JS 将终止查找并使用这个属性,我们用代码来实践下

const MyConstructor = function () {
  this.a = 1
}

const demo = new MyConstructor()
// demo 没有 a,使用原型的 a
console.log(demo.a) // 1
// demo 增加 b 属性
demo.b = 'demo'
console.log(demo.b) // demo
// 原型增加 b 属性
demo.__proto__.b = 2
// 优先使用 demo.b,而不是原型的 b
console.log(demo.b) // demo
delete demo.b
// demo.b 被删除后,使用原型的 b
console.log(demo.b) // 2

继承

我们提到过原型链的继承,不过需要注意的是,JS 的继承并不像 Java 一样,子类全新构建一个新对象,里面含有父类的属性和方法。而是建立一个关联,子类并不会复制父类的属性和方法,而是在要用到的时候从父类调用。

原型用处

看了这么久,原型有什么用呢?似乎我们从来没有用到过,但是原型早已贯彻整个 JS 系统,当你定义一个字符串的时候,原型链就出来了。通过原型,ES 6 实现了 class 语法,当你在使用 class 的时候,其实就是在使用原型的语法糖。

发表评论

电子邮件地址不会被公开。