prototype
大部分「函数数据类型」的值都具备 “prototype”(原型/显示原型) 属性,属性值本身是一个对象,浏览器默认会为其开辟一个堆内存,用于存储实例可调用的公共属性和方法,在浏览器默认开辟的这个堆内存中「原型对象」,有一个默认的属性 constructor(构造器),属性值就是当前函数/类本身。
在 JS 中「函数数据结构」大致有这几种:
- 普通函数(匿名函数/实名函数)
- 箭头函数
- 构造函数/类
- 生成器函数 Generator
- 等等…
但是并不是所有的函数都具备”prototype”(原型/显示原型) 属性,例如:
- 箭头函数 ```javascript var fn = () => {} fn.prototype // => undefined
- ES6 快捷给对象的某个成员赋值函数值
```javascript
var obj = {
fn1: function() {},
fn2() {}
}
obj.fn2.prototype // => undefined
proto
每一个「对象数据结构」的值都具备一个属性”proto“(原型链/隐式原型),属性值指向自己所属类的原型 prototype。
- 普通对象
- 特殊对象:数组、正则、Error、Date …
- 函数对象
- 实例对象
- 构造函数.prototype
- …
练习题
深圳虾皮一面笔试题
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function() {
console.log(this.x);
}
Fn.prototype.getY = function() {
console.log(this.y);
}
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
全局,变量提升,代码执行,具体的细节我这里就不多赘述了,想知道具体过程的,可以去看我前面的「前端基石」的文章,有详细的介绍。这里主要是捋清楚在示例代码执行过程中的「原型链」,也是借用代码的执行来深入理解 prototype 和 proto。
Fn 函数
函数在存储在堆内存中,大致有三部分组成。
- 作用域
- 代码字符串
- 键值对
我们前面说道,「大部分函数类型」都有一个 prototype 属性,属性值本身是一个对象,浏览器默认会为其开辟一个堆内存,用于存储实例可调用的公共属性和方法。
在浏览器默认开辟的这个堆内存中「原型对象」,有一个默认的属性 constructor(构造器),属性值就是当前函数/类本身。
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
上面的代码执行完成之后,存储结构就如下: Fn 函数的 prototype 指向了 Fn 的原型对象。
Fn.prototype
Fn.prototype.getX = function() {
console.log(this.x);
}
Fn.prototype.getY = function() {
console.log(this.y);
}
向 Fn.prototype 的内存中添加方法 getX、getY,其实可以简单的将 Fn.prototype 理解为一个对象,上述代码就是在给一个对象添加方法。 getX、getY 也都是一个函数,js 也会在堆内存中为他们分片一块内存空间。getX、getY 在内存的存储和 Fn 的存储结构大致一致。这里就简单略过。
实例
let f1 = new Fn;
let f2 = new Fn;
创建构造函数实例,构造函数执行,将函数 Fn 当做一个类,new Fn 也就是创建一个构造函数的实例返回,然后赋值给变量。
一般情况下,new Fn 和 new Fn() 基本一样,但是在一些特殊情况是有区别的。例如:new Fn.getX 和 new Fn().getX,new Fn.getX 是先获取 Fn 的 getX 属性在进行 new 操作,new Fn().getX 是先执行 new Fn() 返回一个实例,在获取实例上的 getX。
在构造函数执行是和普通函数执行一样都会初始化作用域链,但是构造函数执行时,初始化作用域链之前,浏览器会默认先创建一个对象(空对象,Fn 的实例对象)。并且在初始化 this 时,会将 this 创建的空对象,所以在后期代码中执行 this.xx = xx 的时候,实际上就是在往这个对象(实例对象)上添加属性或者方法,这里还需要注意下,构造函数执行,函数内部的私有变量和实例是没有关系的(构造函数的执行流程可看上一篇文章前端基石:构造函数和普通函数)。
经过构造函数的执行,创建了两个实例对象 f1、f2,并且在方法资深通过 this.xxx 为实例对象添加了私有属性和方法。
这里有一点需要注意,属性是公有还是私有是一个「相对的」。 例如: Fn.prototype 是原型对象,f1 是实例对象。 getY 相对于原型对象来说是自己的私有方法,但是相对于实例对象来说是实例对象的共有方法。
每一个「对象数据结构」的值都具备一个属性”proto“(原型链/隐式原型),属性值指向自己所属类的原型 prototype。所以实例对象 f1、f2 也都具备这个属性。但是注意”proto“这个属性在新版本浏览器中被叫做[[prototype]]。
Object 内置类
在 js 有很多的内置类,其中有一个内置类「Object 内置类」,所有对象都是 「Object 内置类」的实例。所以最后无论 proto(原型链)如何查询,都最后找到 Object.prototype 上。Object.prototype 也是一个对象,当然也有 proto属性,但是我们说「proto属性值指向自己所属类的原型 prototype」,对于 Object.prototype 来说就是指向“自己”,所以在这里将 proto设置为 null。
将 Object 内置类带入我们当前的场景下,结构如下:
原型链查找机制
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();
在进行结果输出之前我们先理解一个概念「原型链查找机制」。在 js 中 「对象.xxx」进行对象成员访问时:
- 先查询属性或者方法是否是对象本身私有的属性和方法
- 如果不是私有,基于 proto查找所属类 prototype 上的属性和方法
- 如果所属类 prototype 上没有,则基于原型对象上的 proto在向上查找,直到查找到 Object.prototype 为止
这就是「原型链查找机制」,理解了这个概念之后,结合上面画的图,再来进行上诉代码的结果输出:
console.log(f1.getX === f2.getX); => false
f1.getX:getX 是对象 f1 私有的。f2.getX :getX 也是对象 f2 私有的,既然都是私有的,在存储中指向不同的内存地址,所以结果为 false。
console.log(f1.getY === f2.getY); => true
f1.getY:getY 不是 f1 私有的,基于 proto查找所属类 prototype 上的属性和方法,也就是查找 Fn.prototype,Fn.prototype 上存在 getY。同理,f2.getY 通过原型链查找到 Fn.prototype 上的 getY。二者都找到 Fn.prototype 上的 getY ,所以结果是 true。
console.log(f1.__proto__.getX === f2.getX); => false
console.log(f1.__proto__.getY === Fn.prototype.getY); => true
console.log(f1.getX === Fn.prototype.getX); => false
对象.proto.xxx 或者 构造函数.prototype.xxx 都是跳过私有的查找,直接找原型对象上的公有的属性或者方法。
f1.proto.getX 找到的是 原型上共有的 getX,f2.getX 查找到的是自己私有的 getX,它们指向不同的内存空间,所以结果是 false。
f1.proto和 Fn.prototype 都是跳过私有找共有,.getY 都找到共有的 getY,所以结果是 true。
f1.getX 查找私有的 getX,Fn.prototype.getX 查找公有的 getX,所以结果是 false。
console.log(f1.constructor); => [Function: Fn]
console.log(Fn.prototype.__proto__.constructor); => [Function: Object]
f1.getX(); => 100
f1.__proto__.getX(); => undefined
f2.getY(); => 200
Fn.prototype.getY(); => undefined
这一些列的查找其实和前面的例子差不多,按照原型链查找机制进行查找,先找自己私有,在基于 proto进行原型链的查找,直到查找到 Object.prototype 为止,如果还是没有找到,那结果就是 null。当然这里还中和了一些 this 指向的问题,如果你对 this 的指向还不是很清楚,可以去看看我之前的文章前端基石:this的几种基本情况。
这里随带给大家推荐一个网站「https://pythontutor.com/」,利用它可以让我们的代码执行可视化。