一文彻底理解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 相关文章推荐
JavaScript实现禁止后退的方法
Dec 27 Javascript
jQuery 1.5.1 发布,全面支持IE9 修复大量bug
Feb 26 Javascript
javascript 进阶篇2 CSS XML学习
Mar 14 Javascript
jQuery Deferred和Promise创建响应式应用程序详细介绍
Mar 05 Javascript
php析构函数的具体用法小结
Mar 11 Javascript
javascript多物体运动实现方法分析
Jan 08 Javascript
纯原生js实现贪吃蛇游戏
Apr 16 Javascript
protractor的安装与基本使用教程
Jul 07 Javascript
javascript流程控制语句集合
Sep 18 Javascript
vue组件实现可搜索下拉框扩展
Oct 23 Javascript
vue页面加载时的进度条功能(实例代码)
Jan 13 Javascript
Jquery高级应用Deferred对象原理及使用实例
May 28 jQuery
javascript遍历对象的五种方式实例代码
Oct 24 #Javascript
低门槛开发iOS、Android、小程序应用的前端框架详解
Oct 16 #Javascript
基于angular实现树形二级表格
ajax请求前端跨域问题原因及解决方案
浅谈TypeScript 索引签名的理解
JavaScript 反射学习技巧
Oct 16 #Javascript
JS的深浅复制详细
Oct 16 #Javascript
You might like
PHP函数utf8转gb2312编码
2006/12/21 PHP
CMSPRESS 10行代码搞定 PHP无限级分类2
2018/03/30 PHP
php提取微信账单的有效信息
2018/10/01 PHP
JQuery入门—编写一个简单的JQuery应用案例
2013/01/03 Javascript
深入理解JavaScript系列(38):设计模式之职责链模式详解
2015/03/04 Javascript
javascript实现简单查找与替换的方法
2015/07/22 Javascript
Nodejs获取网络数据并生成Excel表格
2020/03/31 NodeJs
超链接怎么正确调用javascript函数
2016/05/23 Javascript
EasyUI布局 高度自适应
2016/06/04 Javascript
快速实现jQuery多级菜单效果
2017/02/01 Javascript
JS中关于正则的巧妙操作
2017/08/31 Javascript
React + webpack 环境配置的方法步骤
2017/09/07 Javascript
浅谈jquery fullpage 插件增加头部和版权的方法
2018/03/20 jQuery
在iFrame子页面里实现模态框的方法
2018/08/17 Javascript
webpack打包多页面的方法
2018/11/30 Javascript
浅谈React Native 传参的几种方式(小结)
2019/05/21 Javascript
[01:06:07]2014 DOTA2国际邀请赛中国区预选赛5.21 DT VS CIS
2014/05/22 DOTA
[48:48]VGJ.T vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
【Python】Python的urllib模块、urllib2模块批量进行网页下载文件
2016/11/19 Python
python 高效去重复 支持GB级别大文件的示例代码
2018/11/08 Python
linux下安装python3和对应的pip环境教程详解
2019/07/01 Python
Python FtpLib模块应用操作详解
2019/12/12 Python
使用py-spy解决scrapy卡死的问题方法
2020/09/29 Python
详解CSS3选择器:nth-child和:nth-of-type之间的差异
2017/09/18 HTML / CSS
移动web模拟客户端实现多方框输入密码效果【附代码】
2016/03/25 HTML / CSS
Stokke美国官方网店:高级儿童家具、推车、汽车座椅和配件
2020/06/06 全球购物
如何高效率的查找一个月以内的数据
2012/04/15 面试题
行政助理工作职责范本
2014/03/04 职场文书
开学典礼主持词
2014/03/19 职场文书
2014组织生活会方案
2014/05/19 职场文书
学习焦裕禄同志为人民服务思想汇报
2014/09/10 职场文书
教师批评与自我批评总结
2014/10/16 职场文书
高中开学感言
2015/08/01 职场文书
小学教师师德培训心得体会
2016/01/09 职场文书
python实现ROA算子边缘检测算法
2021/04/05 Python
Python 游戏大作炫酷机甲闯关游戏爆肝数千行代码实现案例进阶
2021/10/16 Python