前言
最近看了几篇关于原型链的文章,有所收获,但是总是很容易把自己绕蒙了,于是按照自己的理解重新整理了一下
原型链
概述
-
所有的类都是函数数据类型的(包括内置类),于是具有一下性质 ---- 稍后解释
-
所有的函数都天生自带一个属性:
prototype原型 -
prototype的属性值默认是一个对象数据类型值【堆】 -
在对象
prototype中存储的是供实例调用的公共属性和方法 -
并且在类的
prototype原型上,默认有一个属性constructor构造函数,属性值是当前类本身
-
说明:
Array 是一个类
typeof Array // Function
Array 中含有 prototype 属性
prototype 中存储的是 供实例调用① 的公有属性和操作数组的方法(push、pop、splice)
Array.prototype 存在属性constructor,值为: ƒ Array() { [native code] }
①:在创建数组时通过 new 创建的,所以我们使用的数组是Array的一个实例,也就是下面的arr,则arr.push('2')... 这就是上面所说的供实例调用。
let arr = new Array([1,2,3]);
-
所有对象数据类型值也天生自带的一个属性:
__ proto__原型链-
对象数据类型值:
普通对象、数组对象、正则、日期对象、类数组对象、
DOM对象... 大部分实例(除基本数据类型外)也是对象类型「 万物皆对象,所有的值都是对象类型的 」
-
__proto__属性的属性值:表示当前实例所属类的原型prototype实例.__proto__ === 类.prototype
-
说明:
let arr = new Array([1,2,3]);
arr 是 Array 的实例,也是一个对象,因此arr具有__ proto__属性
arr.__ proto__ === Array.prototype && 在前面说过prototype是对象,所以arr.__ proto__也是对象,所以__ proto__也含有__ proto__属性...
举例说明
// a
function Fn(){
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x)
}
}
// b
Fn.prototype.getX = function () {
console.log(this.x)
}
// c
Fn.prototype.getY = function () {
console.log(this.Y)
}
// d
let f1 = new Fn;
let f2 = new Fn;
// e
f1.getX();
f2.getY();
f1.__proto__.getX();
Fn.prototype.getY();
接下来,我会按照代码顺序进行说明,可能会细致一下,请耐心看完~
a. 1. 首先函数Fn会创建一个堆内存,内部存储代码字符串,以及包含prototype原型
a. 2. prototype原型对象中存储 constructor属性,属性值为 Fn
b.1. 在Fn的原型上添加属性getX
c.1. 在Fn的原型上添加属性getY
d.1. 创建实例f1,由实例.__proto__ === 类.portotype得 (如下图)
d.2. 创建实例f2,由实例.__proto__ === 类.portotype得 (如下图)
上图中Fn.prototype原型对象中__proto__没有指向,因此我们可以这样理解, ② 在不知道new谁出来的对象下多数的对象类型值都是Object内置类的一个实例,所以实例的__proto__指向Object的原型prototype
解释一下为什么最后Object.prototype.__proto__ 的值为null,这是因为我们上面说的 ②,并且Object 是所有对象数据类型的'基类',所以此处应该指向自己本身,这样毫无意义,所有最后的值为null
e.1. f1.getX()执行,在执行之前我们先明确一下实例.方法的执行顺序:
-
首先看是否为自己的私有属性,如果是自己私有的,那么操作的就是私有的
-
如果不是自己私有的,默认基于
__proto__找到所属类原型上的公共属性和方法 -
如果原型上也没有,则继续基于
原型.__proto__向上查找,直到找到Object.prototype为止 -
我们把这种查找机制称为'原型链查找机制'
实例.__proto__.方法 或者 类.prototype.方法 的执行顺序如下:
-
跳过私有属性的查找,直接找所属类原型上公共的属性方法
-
注意:
__proto__在IE浏览器中被保护起来了,不允许我们访问操作
e.2. f1.getX()执行
-
f1.getX基于原型链查找机制,找到这个方法(私有方法) -
把找到的方法执行,确定方法中的
this[点前面是谁this就是谁] -
代码执行算出答案皆可
-
私有
getX执行,方法中的this是f1,=> console.log(this.x) => f1.x => 100
e.3. f2.getY()执行
f2.getY基于原型链查找机制,找到公共属性getY执行,console.log(this.Y) => f2.Y => 200
e.4. f1.__proto__.getX()执行
- 执行公共的
getX;this为f1.__proto__,而在f1.__proto__上找不到x,所以f1.__proto__.x为undefined
e.5 Fn.prototype.getY()执行
- 执行公共的
getY,this为Fn.prototype,在Fn.prototype上找不到y,所以Fn.prototype.y为undefined
原型重定向
首先看下一段代码,分析一下:
// a
function Fun() {
this.a = 0;
this.b = function () {
alert(this.a);
};
}
Fun.prototype = {
b: function () {
this.a = 20;
},
c: function () {
this.a = 30;
alert(this.a)
}
};
// b
var my_fun = new Fun();
my_fun.b();
my_fun.c();
a. 首先创建一个函数Fun,然后在Fun的原型上进行了重新的复制操作,所以此时Fun.prototype便指向了新创建的对象,这一步重新操作就是原型重定向
灵魂拷问
1.为什么要重定向 ?
答:为了方便批量给原型上扩充属性和方法
说明:我们通常在给原型上扩展方法时通常是这样写的③,不过当想批量的添加属性时代码就过于'累赘',所以这就是为什么要有原型重定向④
// ③
Fun.prototype.d = function () {}
Fun.prototype.e = function () {}
Fun.prototype.f = function () {}
...
// ④
Fun.prototype = {
d: function () {},
e: function () {},
f: function () {},
...
}
- 原型重定向带来的问题:
-
新定向的原型对象上没有
constructor属性,结构不完整 -
浏览器默认生成的原型对象因为缺少引用会被释放,可能导致原始加入的属性和方法丢失掉
-
注意:内置类的原型使不允许重定向的,可能会导致内置的方法全部消失
b. 1. 创建实例var my_fun = new Fun()
b.2. 执行my_fun.b()
my_fun.b()按照原型链查找机制,b是私有的,this是my_fun,alert(this.a)为字符串0
my_fun.c()按照原型链查找机制,在实例my_fun中并没有c,继续按照my_fun.__proto__向上查找,找到c并执行,this.a = 30,this为my_fun,所以此时实例中a 变为30,alert(this.a)值为字符串30
重写new
首先我们来先了解一在new的过程中都做了什么
function Dog(name) {
this.name = name;
}
let dog = new Dog('aa')
通过打印实例对象dog我们可以总结出一下几点:
-
首先创建一个实例对象,将
实例对象.__proto__ === 类.prototype -
在实例对象中存在name属性,说明将
类Dog按照普通函数执行(私有上下文、作用域链、形参赋值、变量提升、代码执行...) -
类中的
this指向实例对象 -
最后看方法的返回结果,如果没有
return或者返回基本数据类型,则返回实例对象,如果不是,则已返回值为主
按照下面代码执行结果来重写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(); // my name is三毛
sanmao.bark(); // wangwang
说明:首先由一个Dog类,在其prototype上添加了bark和sayName属性
// func -> 类
// ...params -> 基于剩余运算符 传递给类的实参
function _new(func, ...params){
// 创建一个实例对象
let obj = {};
// 实例对象.__proto__ = 类.prototype
obj.__proto__ = func.prototype;
// 改变this指向
let result = func.call(obj, ...params);
// 根据返回结果类型 决定返回结果
if(result !== null && (typeof result==='object' || typeof result==='function') ){
return result
}
return obj
}
let sanmao = _new(Dog, '三毛');
console.log(sanmao, 'sanmao');
sanmao.sayName();
sanmao.bark();
从上面的截图可以看出,我们已经实现了new的功能,但是在IE浏览器中对于__proto__不兼容,所以接下来进一步优化解决这个问题
let obj = {
name: 'haha'
}
console.log(Object.create(obj))
Object.create([对象A]):创建一个空对象,并且将对象A作为它的原型 {}.__ptoro__ === 对象A
Object.create(): 报错 Object prototype may only be an Object or null
Object.create(null): 创建一个空对象,但是此对象的__proto__等于null(没有),此对象不再是任何类的实例
此时的new重写为
// func -> 类
// ...params -> 基于剩余运算符 传递给类的实参
function _new(func, ...params){
// 创建一个实例对象
// 实例对象.__proto__ = 类.prototype
let obj = Object.create(func.prototype);
// 改变this指向
let result = func.call(obj, ...params);
// 根据返回结果类型 决定返回结果
if(result !== null && (typeof result==='object' || typeof result==='function') ){
return result
}
return obj
}
let sanmao = _new(Dog, '三毛');
console.log(sanmao, 'sanmao');
sanmao.sayName();
sanmao.bark();
既然已经看到了这里我么在深入一下,new重写已经实现了,在其中我们使用了Object.create(),我们也来实现一下:
Object.create()是ES2015提供的方法,不兼容低版本浏览器
Object.create = function create(obj){
if(obj === null || typeof obj !== 'object'){
throw new TypeError('Object prototype may only be an Object')
}
function Anonymous() {};
Anonymous.prototype = obj;
return new Anonymous;
}
总结
篇幅略长,不知道看到这里有没有蒙了🤔 对于原型链我个人的理解是首先了解其原型链查找机制,然后是原型重定向,最后要了解在new过程中都做了什么,为了我们来更好的理解原型链。
如果有什么错误的地方希望不吝啬指出,及时改正✊
最后
biu biu biu ~ ❤️❤️❤️❤️ 点关注 🙈🙈 不迷路 点个赞哦😘