承接上一篇面向对象基础知识——原型与原型链基础知识
下面从两个角度分析解释原型是什么,原型链机制又是如何运作的
从两个角度先说说概念
从函数数据类型角度分析
大部分函数数据类型的值都具备prototype
(原型/显式原型)属性,属性值本身是一个对象。浏览器会默认为其开辟一个堆内存,用来存储当前类所属实例可以调用的公共的属性和方法。在浏览器默认开辟的这个堆内存(原型对象)中,有一个默认的属性 constructor
(构造函数/构造器),属性值是当前函数/类本身。
函数数据类型分类
- 普通函数(实名或者匿名函数)
- 箭头函数
- 构造函数/类「内置类/自定义类」
- 生成器函数
Generator
- ...
不具备prototype
的函数
- 箭头函数
const fn=()=>{}
- 基于ES6给对象某个成员赋值函数值的快捷操作
let obj = { fn1: function () { // 常规写法 具备prototype属性 }, fn2() { // 快捷写法 不具备prototype属性 } }; class Fn { fn() {} //这样的也不具备prototype属性 };
从对象数据类型值角度分析
每一对象数据类型的值都具备一个属性__proto__
(原型链/隐式原型),属性值指向自己所属类的原型
prototype
。
ps:这里先不考虑函数类型的对象
对象数据类型值分类
- 普通对象
- 特殊对象:数组、正则、日期、
Math
、Error
... - 函数对象
- 实例对象
- 构造函数。prototype
面向对象的底层都是基于这两个角度:类(构造函数)和实例对象
总结
记住这两个最重要的概念:
- 大部分函数数据类型的值都具备
prototype
(原型/显式原型)属性,属性值本身是一个对象。浏览器会默认为其开辟一个堆内存,用来存储当前类所属实例可以调用的公共的属性和方法。在浏览器默认开辟的这个堆内存(原型对象)中,有一个默认的属性constructor
(构造函数/构造器),属性值是当前函数/类本身 - 每一对象数据类型的值都具备一个属性
__proto__
(原型链/隐式原型),属性值指向自己所属类的原型prototype
。
举例理解概念
举例一个内置类(数组类Array
和其实例)来理解以上概念
函数数据类型角度理解
我们知道,任何函数在内存中会存在:作用域,代码字符串,属性键值对,那我们来看看Array
-
作用域
因为是内置的,作用域不清楚,但是是有作用域的
-
代码字符串
[native code]
为代码字符串,可能为C++写的,浏览器内置代码不让我们不看 -
属性键值对中的
proptotype
属性 每一个函数都天生具备一个proptotype
,所以Array
中也是有的并且
proptotype
对象中有一个constructor
属性指向函数本身,另外包含很多Array
的公共属性(concat
,forEach
等),如图:总结:
同样,Object
函数也是一样
对象数据类型角度理解
ps:函数也是对对象,关于函数多种角色的问题这里暂时先不讨论,后面写文章进行补充
实例对象,prototype
(原型对象),函数对象,都是对象,所有的对象都有__proto__
属性,这个属性指向所属类的原型对象。
指向关系如下
注意容易混淆的点:
原型对象并不是他所属的构造函数的一个实例,比如Array.prototype
不是Array
的一个实例。因为既然作为原型,他是所有实例用来共享属性和方法的对象,所以不会是其中的一个实例。只有new
出来的才算是某个构造函数的实例。Array
的原型对象是浏览器默认开辟出来的,这里不是Array
构造函数的实例。
通过浏览器我们发现Array.prototype.__proto__
指向Object.proptotype
,所以Array.prototype
是Object
的实例
注意更加特殊的Object.prototype.__proto__
,指向null
。因为Object
是所有对象的基类,Object.prototype
本身就是一个对象,Object.prototype.__proto__
最后只能指向自己,即Object.prototype
,这是没有意义的,所以最后浏览器让其指向null
,这样也更合理一点。
那么以上原型链机制的定义明确了以后,平常是怎么运作的呢?举个例子理解:
执行arr.length
,查看arr
的属性length
,或者执行arr.push
的过程:
首先访问自己的私有属性,如果私有属性中是存在的,则直接使用。如果访问的成员在私有属性中没有,默认会基于__proto__
找到所属类的prototype
上的属性/方法。
所以Array.prototype
上的方法,例如push
等,相对于Array
的实例来说,算是共有属性
执行或使用arr.hasOwnProperty
的过程:
私有没有,Array.prototype
上也没有,所以会继续基于Array.prototype.__proto__
继续往上寻找,最终在Object.prototype
上找到该方法,如果再找不到就返回undefined
。
我们把上面这种成员访问的查找机制叫做原型链机制
如下图:
以上是最常用的成员访问机制,还有一些其他方法进行成员访问:
例如arr.forEach()
,除了直接这样使用,还可以:
- 可以直接使用
arr.__proto__.forEach()
这样可以直接跳过私有属性的查找,直接使用原型对象上的方法(此方法平时一般不会自己手动操作,因为ie浏览器没法使用这个方法,ie将其保护起来,不允许我们访问) - 也可以
Array.prototype.forEach()
,也可以直接使用原型对象上的方法
那么这几种方法的区别是什么?三者都是找到Array.prototype.forEach
,并且让其执行,区别在于forEach
方法中的this
指向不一样。谁调用了方法,点前面是什么,this
就是什么。以上三种方式的this
分别是arr
,arr__proto__
,Array。prototype
如下例子:
let arr = [10, 20, 30];
console.log(arr.hasOwnProperty('forEach')); //->false
简单来看,上面的代码是 验证forEach
是否为arr
对象的私有属性
那么整个的执行过程实际上是:
arr
按照原型链查找机制,找到的是Object.prototype.hasOwnProperty
方法「@A」,并且把找到的@A执行,注意:
- @A方法中的
this
应该是arr
「我们要操作的对象」 - 传递的实参应该是
forEach
「我们要验证的属性」 @A方法的作用是,验证“实参”是否为当前this
的一个私有属性 ,那么最终同等效果的执行方式为:===>Object.prototype.hasOwnProperty.call(arr,'forEach')
公有属性和私有属性的"相对"
另外需要注意的点:公有属性和私有属性是"相对"的。
例如相对于arr
这个实例对象,Array.prototype
这个对象里面的push
等方法是各个arr
实例的公有属性。而Array.prototype
他自己本身就是一个对象,push
在他这里就是他自己的私有方法,所以对象上的属性是共有还是私有,得要相对来说。当做公有属性,是相对于类的实例来说,当做私有属性是相对于自身来说
所以没有公有属性,私有属性这个严格的概念,而只是相对来说,当不同的角色有了不同的功能,最后才区分了公有,私有的概念。
这样明白以后,我们在写代码进行操作的时候,例如我们做一个字符串截取工作,那么我们可以直接查看String.prototype
上有什么公有方法可以直接调用来使用,然后还可以顺着__proto__
继续往上找公有方法,按照原型连一级级往上找,只要出现在原型链上的方法都可以使用。
例如我们一直往上找document.body
实例的方法,发现其有事件相关的方法
这样我们就可以使用document.body.addEventListener
,再例如dom操作都有的classList
方法,用来操作class
都可以找到相关的方法
所以,整个JS就是基于面向对象思想构建的
加深理解的例子
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);//false
console.log(f1.getY === f2.getY);//true
console.log(f1.__proto__.getY === Fn.prototype.getY);//true
console.log(f1.__proto__.getX === f2.getX);//false
console.log(f1.getX === Fn.prototype.getX);//false
console.log(f1.constructor);//Fn
console.log(Fn.prototype.__proto__.constructor);//Object
f1.getX();//100
f1.__proto__.getX();//undefined
f2.getY();//200
Fn.prototype.getY();//undefined
注:new Fn()
和new Fn
是一样的效果,只是前者优先级是20,可传参数,后者优先级是19,不传参数
分析:
f1和f2都如下,迷惑点在于,其私有属性和原型上都有getX
函数,两个私有属性地址不一样,返回false,而原型是同一个对象,其中的getX
函数是同一个地址
__proto__
返回原型
重写内置new
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
console.log('wangwang');
}
Dog.prototype.sayName = function () {
console.log('my name is ' + this.name);
}
/*
let sanmao = new Dog('三毛');
sanmao.sayName();
sanmao.bark();
*/
function _new() {
//=>完成你的代码
}
let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true
new
关键字做了什么:
- 创建Ctor的一个实例对象(创建一个实例对象,并将实例对象的
__proto__
指向Ctor.prototype
) - 把构造函数当做普通函数执行,并且让方法中的
this
指向实例对象 - 确认方法执行的返回值。如果没有返回值或者返回的是原始值,让其默认返回实例对象即可。
注:
-
Ctor ->
constructor
缩写 构造函数 -
params -> 后期给Ctor传递的所有的实参信息
function _new(Ctor, ...params) {
// 1.创建Ctor的一个实例对象
// 实例.__proto__===Ctor.prototype
let obj = {};//这里仅仅只创建了Object的一个实例对象
obj.__proto__ = Ctor.prototype;
// 2.把构造函数当做普通函数执行「让方法中的THIS->实例对象」
let result = Ctor.call(obj, ...params);
// 3.确认方法执行的返回值「如果没有返回值或者返回的是原始值,我们让其默认返回实例对象即可」
if (result !== null && /^(object|function)$/.test(typeof result)) return result;
return obj;
}
创建Ctor的一个实例对象 如果用
let obj = {};//这里仅仅只创建了Object的一个实例对象
obj.__proto__ = Ctor.prototype;
上面这种方法不太好,下面说说新的方法
Object.create()
Object.create([obj])
:创建一个空对象,并且让空对象.__proto__
指向 obj
。
官方解释:把obj
作为新实例对象的原型。
也这样解读:以obj
为原型,创造一个新对象
注意:
- 参数
obj
可以是一个对象或者是null
,但是不能是其他的值 Object.create(null)
创建一个不具备__proto__
属性的对象(不是任何类的实例)
重写Object.create()
Object._create = function create(prototype) {
if (prototype !== null && typeof prototype !== "object") throw new TypeError('Object prototype may only be an Object or null');
var Proxy = function Proxy() {}
Proxy.prototype = prototype;
return new Proxy;
};
效果:
需要注意的是,我们自己写的这个_create
,在传入null
的时候是无法消除__proto__
的
__proto__
是没办法消除的,无法删除,并且直接指向Object
允许使用Object.create()
时重写new
使用Object.create()
可替代重写new
的第一步
将变量前置声明,更加规范,并注意一些校验规则
function _new(Ctor, ...params) {
let obj,
result,
proto = Ctor.prototype,
ct = typeof Ctor;
// 构造函数的校验规则
//前提不能是Symbol或者BigInt,然后Ctor不是函数或者proto不存在(箭头函数)就抛出一个类型错误
if (Ctor === Symbol || Ctor === BigInt || ct !== 'function' || !proto) throw new TypeError(`${Ctor} is not a constuctor!`);
//符合规则后按逻辑来
//1.
obj = Object.create(Ctor.prototype);
//2.
result = Ctor.call(obj, ...params);
//3.
if (result !== null && /^(object|function)$/.test(typeof result)) return result;
return obj;
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!