一文彻底理解js原生语法prototype,__proto__和constructor


Posted in Javascript onOctober 24, 2021

1 前言

写了几篇vue的源码注释(并不算解析...), 感觉到了对原型的理解是不够的, 在js中, 原型是非常重要的, 只要你想在js这座山上往上爬, 它就会嘲笑你, 你把我搞会了么? 如果没有, 它就给你加个十倍重力. 如果搞懂了, 那肯定是能月薪过万, 赢取白富美, 走向人生巅峰的啦~~~


这篇文章讲的都是我自己的理解, 应该是原创的(我有99%把握, 除非是我之前看过文章记到脑子里了, 没法给到引用了, 联系我可以加上), 但是如果有人借鉴我的这篇文章, 希望给到一个这篇文章的链接. 其实我只是想我的文章能有更多的阅读量, 我想月薪过万, 赢取白富美, 走向人生巅峰~~~

2 前置知识点

2.1 数据类型

js共有7种数据类型

从可不可以读取属性, 可以分为两类

  • 可以读取属性:
    • 自身可以有属性: object
    • 自身不可以有属性: string,number,boolean,symbol
  • 不可以读取属性: null,undefined

null,undefined类型, 读取和设置属性都是非法的, 直接报错.

只有object能有自有属性, 可以读取属性和设置属性

string,number,boolean,symbol类型可以读取属性, 其实是先构造成包装对象, 再读取属性, 设置属性也是一样, 可以理解设置到了会立即销毁的包装对象上, 就是可以设置, 但是没有任何实质效果.

2.2 判断是否是自身属性(hasOwnProperty)

hasOwnProperty方法是继承来的, 用来判断该对象自身上是否有这个属性, 有就行, 不管是什么值

const obj = { a: 1 }
const o = Object.create(obj)
o.b = 1
o.c = void 0
console.log('a', o.a, o.hasOwnProperty('a')) // 可以读取到值, 继承而来, 但不是自身属性
console.log('b', o.b, o.hasOwnProperty('b')) // 可以读取到值, 自身属性
console.log('c', o.c, o.hasOwnProperty('c')) // 读取到undefined, 自身属性
console.log('d', o.d, o.hasOwnProperty('d')) // 读取到undefined, 不是自身属性, 也没有继承到这个属性

一文彻底理解js原生语法prototype,__proto__和constructor

3 一点小思考

程序就是数据结构与算法, 好的程序最好是使用最小的内存存储数据, 使用最快的时间完成运行得到结果.

复用数据可以达到减少内存使用的目的, 例如a和b需要完成一样的功能, 就可以复用同一个方法(属性).

那么就需要解决一个问题, 这个复用的方法存在哪里, a和b怎样能找到它.

在js中的解决方案是, a和b都由函数(这里先叫Function吧)构造而来, 复用的方法存放在函数身上(prototype属性里).

(因为只有构造函数身上需要存放复用的方法, 所以prototype只有可构造的函数上才有, 箭头函数不是用来构造的, 它就没有, 其它对象, 如果连函数都不是, 就更不会有这个属性了)

那么需要给a,b 和Function建立起联系, 因为a,b需要到Function身上找它们可以用的复用方法

在js中的实现是通过constructor属性,即a.constructor, b.constructor可以找到Function

所以通过a.constructor.prototype可以找到它可以复用的方法的存放地址, 为了快速找到js提供了一种快捷方法a.__proto__一步到位找到, 即a.constructor.prototype和a.__proto__找到的是同一个对象, 当然它俩是全等的啦.

// 它俩都不是自有属性, 我也不知道怎么从这俩属性上找到原型对象的了, 肯定是魔法.....
const obj = {}
console.log(obj.hasOwnProperty('__proto__')) // false
console.log(obj.hasOwnProperty('constructor')) // false

(所以, 如果手动修改了constructor,prototype,__proto__的指向, 那么你得清楚你在干什么)

(我不知道js的设计者是不是这样想的, 哈哈, 我就这样认为, 这样好理解多了)

(这个过程称之为继承, 而且是一个链式过程, 即可以a.constructor.prototype.constructor.prototype.constructor.prototype这样查找, 直到找到最顶端, 这个过程可以由a.__proto__.__proto__.__proto__加速, 这个就叫做原型链, js的继承只有这一种实现方式.)

(上面只是引导思考过程, 其实查找原型对象并不会通过a.constructor.prototype去找, 而是直接通过__proto__查找)

3.1 修改 constructor

const Dog = function () {}
const dog = new Dog()

dog.constructor = 0

console.log(dog.hasOwnProperty('constructor')) // true
console.log(dog.constructor) // 0

console.log(dog.__proto__.constructor) // [Function: Dog]

总结, 修改了这个属性, 增加了找到构造它的构造函数的难度, 不能直接获取了, 需要到原型对象上去读取.

如果它自身的这个属性和原型上的这个属性都被修改了, 那么也只是找不到它的构造函数了而已, 不会有别的影响.

3.1.1 instanceof

instanceof关心的是原型链, 跟constructor没有关系

印证上面的点, 修改constructor属性, 除了让实例找不到构造它的构造函数, 没有别的影响了. 如果需要通过实例找到它的构造函数, 就需要维护好它俩的关系.

// 语法是
// a instanceof b
// 这个操作符是判断 a 的原型链上是否有  b.prototype, 因为要判断 b.prototype 所以 b 必需是一个 可构造的函数, 否则会报错
const fn = function () {}
const o = Object.create(fn.prototype)
// 此时 o 的原型链上有 fn.prototype, 因为 o.__proto__ === fn.prototype
console.log(o instanceof fn) // true
const emptyObj = {}
fn.prototype = emptyObj
// 此时 o 的原型链上已经没有 fn.prototype 了, 因为此时 o.__proto__ 已经不再和 fn.prototype 相等了
console.log(o instanceof fn) // false
o.__proto__ = emptyObj
// 修正了 o.__proto__ 就好了
console.log(o instanceof fn) // true

3.1.2 isPrototypeOf

现在有个新的api, 实现的功能和instanceof一致, 但是更加语义化一些, 直接判断对象是否在另一个对象的原型链上

const fn = function () {}
const o = Object.create(fn.prototype)
console.log(fn.prototype.isPrototypeOf(o)) // true

3.2 修改__proto__|prototype

先说一个总结, 在构造实例的时候, 会将这个实例的__proto__指向此时的构造函数的prototype, 然后实例实际是继承的是__proto__.(为什么强调此时, 因为构造函数的prototype可能会被修改指向, 修改之后只会影响修改之后构造的实例, 修改之前构造的实例还会使用修改之前的prototype)

所以, 就可以理解到修改__proto__和prototype会有哪些影响了

1.修改__proto__的指向

只会影响它自己的继承

const Dog = function () {}
const dog = new Dog()
const d = new Dog()
Dog.prototype.name = 'Dog'
dog.__proto__ = {
  name: '__proto__',
}
console.log(d.name) // Dog
console.log(dog.name) // __proto__

2.修改__proto__的属性

会影响这一波段构造的实例

const Dog = function () {}
const dog = new Dog()
const d = new Dog()
Dog.prototype.name = 'Dog'
console.log(d.name) // Dog
console.log(dog.name) // Dog
Dog.prototype = {
  name: 'after',
}
const dog1 = new Dog()
const d1 = new Dog()
console.log(d1.name) // after
console.log(dog1.name) // after
dog1.__proto__.name = '__proto__'
// 可以看到只影响了当前这一段构造的实例, 之前和之后的都不会被影响到, 因为这一段内的是同一个 Dog.prototype , 它们的 __proto__ 都是指向它的
console.log(d1.name) // __proto__
console.log(dog1.name) // __proto__
Dog.prototype = {
  name: 'new',
}
const dog2 = new Dog()
const d2 = new Dog()
console.log(d2.name) // new
console.log(dog2.name) // new

3.修改prototype的指向

会影响这一波段构造的实例

4.修改prototype的属性

会影响这一波段构造的实例, 同修改 __proto__的属性

4 修改和获取原型对象的方式

4.1 修改

上面已经讲了修改prototype和__proto__

4.1.1 Object.create

const obj = {
  name: 'objName',
}

const o = Object.create(obj)
// 它相当于 o.__proto__ = obj, 但是推荐使用`Object.create`

console.log(o.name) // objName
console.log(o.__proto__ === obj) // true

4.1.2 Object.setPrototypeOf

const obj = {
  name: 'objName',
}

const o = {}

Object.setPrototypeOf(o, obj)
// 它相当于 o.__proto__ = obj, 但是推荐使用`Object.setPrototypeOf`
const proto = Object.getPrototypeOf(o)
console.log(proto === obj && proto === o.__proto__) // true
const obj1 = {}
o.__proto__ = obj1
const proto1 = Object.getPrototypeOf(o)
console.log(proto1 === obj1 && proto1 === o.__proto__) // true

总结, 在什么时候使用Object.create, 在什么时候使用Object.setPrototypeOf呢, 首先它俩都是标准api, 都是建议使用的, 在创建对象的时候就要指定原型时使用Object.create, 需要动态修改原型对象时, 使用Object.setPrototypeOf

4.2 获取

之前已经讲了, 通过 constructor.prototype和__proto__获取了

4.2.1 Object.getPrototypeOf

const obj = {
  name: 'objName',
}

const o = {}

Object.setPrototypeOf(o, obj)

const proto = Object.getPrototypeOf(o)
console.log(proto === obj && proto === o.__proto__) // true

5 js 内置原生构造函数

这些原生的构造函数的prototype属性是不可写, 不可枚举, 不可配置的

console.log(Object.getOwnPropertyDescriptor(Object, 'prototype'))
// {
//   value: [Object: null prototype] {},
//   writable: false,
//   enumerable: false,
//   configurable: false
// }

5.1 js 继承的最顶端是什么

null, 必须是这家伙, 不然只能无限套娃了

然后其它所有对象都是从Object构造而来, 所以所有的对象都可以继承到Object.prototype.

const obj = {}
const o = new Object()

5.2 js 继承的二等公民(Function)

在上面的小思考中, 说到, js对象都是函数构造而来, 所以包括Object也是由Function构造来的, 甚至它自己都是由自己构造而来

console.log(Object.constructor === Function) // true
// 这就离谱了, 第一个Function是从哪里来的呢????
console.log(Function.constructor === Function) // true

我再来一点小理解, 可能是在js内部做了小处理, 第一个Function是凭空变出来的.... 然后这个Function构造出了Object, 然后这个Object构造出了第一个原型对象Object.prototype, 然后再去修改一些引用关系.

其实最复杂的是Object和Function的关系

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

5.3 js 继承的三等公民(内置的其他构造函数)

const arr = [
  String,
  Array,
  Boolean,
  Number,
  Date,
  RegExp,
  Error,
  Promise,
  Map,
  Set,
  Symbol,
  Proxy,
]
// 都是由Function构造而来

6 用户定义的特定公民构造函数

这个才是重点, 根据上面的理解, 我会再开一篇文章写一下我理解的js的继承, 这里就先留个坑

7. 总结

这篇文章跟网上大多讲constructor,prototype,__proto__的文章都有所不同, 我的立足点是从给定的一个可以读取属性的值开始, 在js中, 除了null和undefined, 其它所有的值都可以成为立足点. 从这个立足点开始, 它的__proto__属性记录了它的原型对象, 这个原型对象是构造它时, 它的构造函数的prototype属性的值.

const a = 1
console.log(a.__proto__.constructor) // [Function: Number]

读取一个值的属性的值时, 如果它自身有这个属性, 那么直接返回这个属性的值, 否则就会到它的__proto__对象上去找, 一直递归下去, 直到找到顶部null, 找到就返回它的值, 没找到就返回undefined

这篇文章有三个理解点,让我茅塞顿开, 都是在我试验了好久突然得到的结论

  1. 以一个值为立足点开始分析
  2. 在构造实例的时候, 会将这个实例__proto__指向此时的构造函数的prototype
  3. 查找原型对象时, 以__proto__为准

8 最后

到此这篇关于js原生语法prototype,__proto__和constructor的文章就介绍到这了,更多相关js原生语法prototype,__proto__和constructor内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jQuery 1.2.x 升? 1.3.x 注意事项
May 06 Javascript
jquery中动态效果小结
Dec 16 Javascript
JavaScript DOM节点添加示例
Jul 16 Javascript
使用node.js半年来总结的 10 条经验
Aug 18 Javascript
深入理解JavaScript系列(40):设计模式之组合模式详解
Mar 04 Javascript
jQuery实现自定义右键菜单的树状菜单效果
Sep 02 Javascript
在Ubuntu系统上安装Node.JS的教程
Oct 15 Javascript
JavaScript在网页中画圆的函数arc使用方法
Nov 13 Javascript
JavaScript组件开发之输入框加候选框
Mar 10 Javascript
vue子父组件通信的实现代码
Jul 09 Javascript
微信小程序模板消息限制实现无限制主动推送的示例代码
Aug 27 Javascript
微信小程序自定义弹出层效果
May 26 Javascript
javascript遍历对象的五种方式实例代码
Oct 24 #Javascript
低门槛开发iOS、Android、小程序应用的前端框架详解
Oct 16 #Javascript
基于angular实现树形二级表格
ajax请求前端跨域问题原因及解决方案
浅谈TypeScript 索引签名的理解
JavaScript 反射学习技巧
Oct 16 #Javascript
JS的深浅复制详细
Oct 16 #Javascript
You might like
PHP与SQL注入攻击[三]
2007/04/17 PHP
PHP优于Node.js的五大理由分享
2012/09/15 PHP
php生成二维码时出现中文乱码的解决方法
2014/12/18 PHP
PHP新特性之字节码缓存和内置服务器
2017/08/11 PHP
PHP 模拟登陆功能实例详解
2019/09/10 PHP
在 PHP 和 Laravel 中使用 Traits的方法
2019/11/13 PHP
JavaScript 拾碎[三] 使用className属性
2010/10/16 Javascript
jQuery中绑定事件的命名空间详解
2011/04/05 Javascript
javascript屏蔽右键代码
2014/05/15 Javascript
javascript实现阻止iOS APP中的链接打开Safari浏览器
2014/06/12 Javascript
分享js粘帖屏幕截图到web页面插件screenshot-paste
2020/08/21 Javascript
javascript实现方法调用与方法触发小结
2016/03/26 Javascript
angular.js实现列表orderby排序的方法
2018/10/02 Javascript
mpvue小程序循环动画开启暂停的实现方法
2019/05/15 Javascript
layui实现三级联动效果
2019/07/26 Javascript
vue实现表单录入小案例
2019/09/27 Javascript
浅析vue-cli3配置webpack-bundle-analyzer插件【推荐】
2019/10/23 Javascript
vue过滤器实现日期格式化的案例分析
2020/07/02 Javascript
Python 字符串操作实现代码(截取/替换/查找/分割)
2013/06/08 Python
Python中的rjust()方法使用详解
2015/05/19 Python
tensorflow 1.0用CNN进行图像分类
2018/04/15 Python
对Python3.x版本print函数左右对齐详解
2018/12/22 Python
python sklearn常用分类算法模型的调用
2019/10/16 Python
Django集成celery发送异步邮件实例
2019/12/17 Python
python设置环境变量的作用整理
2020/02/17 Python
python实现字符串和数字拼接
2020/03/02 Python
使用Python获取当前工作目录和执行命令的位置
2020/03/09 Python
详解anaconda离线安装pytorchGPU版
2020/09/08 Python
Python监听剪切板实现方法代码实例
2020/11/11 Python
香港个人化生活购物网站:Ballyhoo Limited
2016/09/10 全球购物
企业安全生产演讲稿
2014/05/09 职场文书
单位委托书怎么写
2014/08/02 职场文书
大学生自荐材料范文
2014/12/30 职场文书
教代会开幕词
2015/01/28 职场文书
css position fixed 左右双定位的实现代码
2021/04/29 HTML / CSS
windows10声卡驱动怎么安装?win10声卡驱动安装操作步骤教程
2022/08/05 数码科技