本着包教包会,不会再看几遍就会,忘了再再看几遍也能想来的原则,将 JS 原型部分涉及概念一次说清楚。后续关于 ES6 Class 的语法糖也将基于本篇文章继续更新!
下面这段话来自 MDN,感觉描述的很全面,希望看完本文能够彻底理解下面这段话的含义!
JavaScript 常被描述为一种基于原型的语言。每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)
,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
通过原型这种机制,JavaScript 中的对象从其他对象继承功能特性;这种继承机制与经典的面向对象编程语言的继承机制不同。
准确地说,这些属性和方法定义在 Object 的构造器函数(constructor functions)之上的 prototype 属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是 __proto__
属性,是从构造函数的 prototype 属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
本文大纲共分为三部分:
- 第一部分 理解 JS 原型链
- 第二部分 new 操作符到底做了什么?实现一个 new !
- 第三部分 JS 怎么实现继承的?原型链继承!
下面进入主题~
第一部分 理解 JS 原型链
1. 构造函数、实例对象
- person 是 Person 函数的实例对象
- Person 函数 是 person 的构造函数
function Person() {}
let person = new Person();
person; // Person {__proto__} 是一个对象,有一个__proto__属性
person.constructor == Person; // true 指向Person构造函数本身
constructor
属性其实就是一个拿来保存自己构造函数引用的属性。实例对象的 constructor
属性指向构造函数本身
(其实这么说不准确。后面会说明其实是 person 实例对象通过 __proto__
属性指向了构造函数的原型对象 prototype,prototype 的 constructor 属性才指向了构造函数 Person 本身。所以打印结果上来看 person 上本身是没有 constructor 的,但是却能打印 person.constructor)
2. JS 内置对象 Object、Function
-
Function 函数和 Object 函数是 JS 内置对象,也叫内部类。这些内置函数可以当作构造函数来使用。
-
为什么 “函数即对象”?关于 Function 对象的特殊性。
function Person() {}
Person.constructor == Function; // true 构造函数的 constructor 属性指向内置函数 Function
- 所有函数都是 Function 函数的实例对象,所以常说函数即对象,JS 中一切皆对象。
// 也都指向内置 Function
Function.constructor == Function; // true
Object.constructor == Function; // true
- 说明 Function 函数同时是自己的构造函数
- 说明 Function 函数同样也是 Object 这类内置对象的构造函数。
let obj = {};
obj.constructor == Object; // true 普通对象 constructor 属性都指向 Object 构造函数
- 说明普通对象也都是内置 Object 函数的实例对象。
整理一下 constructor 指向链:
-
person.constructor ——> Person 构造函数
,Person.constructor ——> Function 函数
,Function.constructor ——> Function 自己
-
obj.constructor ——> Object
,Object.constructor ——> Function 函数
,Function.constructor ——> Function 自己
3. 原型对象 prototype
- 为了让实例对象能够共享一些属性和方法,将他们全部放在了
构造函数的原型对象 prototype
上。
function Person() {}
Person.prototype.sayHello = function () {
console.log("Hello!");
};
let person1 = new Person();
let person2 = new Person();
person1.sayHello === person2.sayHello; // true
- constructor 也是一个共享属性,存放在原型对象 prototype 中,作用依然是指向自己的构造函数。
function Person() {...}
Person.prototype.constructor == Person // true
从示意图中看出实例对象
、构造函数
、原型对象 prototype
和 constructor
的存在关系。
实例对象的 constructor 属性指向该实例对象的构造函数,constructor 属性作为共享属性存在于构造函数的 prototype 上。
疑问 ? :前文所说的 person.constructor 指向 Person 构造函数,那么 person 和 constructor 又是如何联系起来的呢?也就是如何让实例对象能找到自己的原型对象呢?↓↓↓
4. __proto__
属性
- 先说结论,这也是理解原型链的核心公式:
person.__proto__ == Person.prototype
。即实例对象的__proto__
属性指向构造函数的原型对象prototype
。
通过在 persion 对象内部创建了一个属性 __proto__
直接指向自己的原型对象 prototype
,通过原型对象就可以找到共享属性 constructor
,此时 constructor
指向该实例对象的构造函数 Person() 了。
重新理解一些开篇的第一个例子,其实应该是这样的~~
function Person() {}
let person = new Person();
// 请背下来!!
person.__proto__ == Person.prototype;
// constructor存在于prototype上,指向构造函数
Person.prototype.constructor == Person;
// 所以person.__proto__可以读取原型prototype上的constructor,指向构造函数
person.__proto__.constructor == Person;
// 那为什么开篇的等式也成立呢?
person.constructor == Person; // true 也成立?
// 这就是因为原型链查找了,后面会详细说。因为person通过原型链查找到了constructor,并且指向了Person构造函数。
// but 注意
Person.constructor == Function; // 因为Person的构造函数是Function
- 关于
__proto__
的第二个重要概念是,由于原型对象 prototype 也是个对象,那它也有个__proto__
属性,指向自己的原型对象。那它的构造函数是谁呢?
Person.prototype.__proto__ == Object.prototype
, 找到了!
所以函数内的原型对象 prototype
跟所有普通对象一样,也都是内置 Object
函数的实例对象。
总结:实例对象的 __proto__
属性指向构造函数的原型对象 prototype
;
而所有原型对象 prototype
的 __proto__
属性,都指向了 Object
的原型对象!
有点绕了,但这正是我们理解原型链的核心原理!!
下面这张图描述了很好地描述了 __proto__
与 prototype 原型对象的关系:
5. 原型链来了
有了上面的基础,接下来理解下原型链到底是什么?
-
每个
实例对象
都有一个私有属性 __proto__
指向它的构造函数
的原型对象 prototype
。该原型对象也有自己的私有属性__proto__
指向原型对象...层层向上直到一个对象(内置Object对象
)的原型对象为null
,并作为这个原型链中的最后一个环节。 -
将一个个
实例对象
和原型对象
关联在一起,关联的原型对象也是别人的实例对象,所以就形成了串连的形式,也就形成了我们所说的原型链。 -
Object 函数是所有对象通过原型链追溯到最根的构造函数。即 “最后一个 prototype 对象”便是 Object 函数内的 prototype 对象了。
-
Object 函数内的 prototype 对象的
__proto__
指向 null。 为什么 Object 函数不能像 Function 函数一样让__proto__
属性指向自己的 prototype?答案就是如果指向自己的 prototype,那当找不到某一属性时沿着原型链寻找的时候就会进入死循环,所以必须指向 null,这个 null 其实就是个跳出条件。
还是上面的那张图,通过 Person 构造函数可以找到四条原型链:!!
第 1 条:persion.__proto__ --> Persion.prototype --> Persion.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null
第 2 条:Persion.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null
第 3 条:Function.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null
第 4 条:Object.__proto__ --> Function.prototype --> Function.prototype.__proto__ --> Object.prototype --> Object.prototype.__proto__ --> null
(“我中有你,你中有我”。。。)
下面我们简单看一下实例对象 p 上都有什么
function P(){};
let p = new P(); p;
// ↓↓↓
> P {}
> __proto__: Object // 实例对象p上有 __proto__ 属性,指向构造函数的原型对象prototype
> constructor: ƒ P() // prototype 上有公共方法 constructor
> __proto__: // prototype 上还有 __proto__ 属性,指向内置Object的原型对象
> constructor: ƒ Object() // 内置Object的原型对象上的constructor 以及hasOwnProperty等公共方法
> hasOwnProperty: ƒ hasOwnProperty()
...
那么构造函数的属性和方法放在 this
上和放在 prototpe
上有什么区别?
其实是都可以的,区别在于 this 定义的属性和方法是生成的实例自己独有的属性和方法;而定义在 prototype 上的属性和方法,是每个实例所共有的。那么定义在 prototype 上的属性和方法发生改变则每个实例对象都会拿到。
举个例子:
function Person() {
this.age = 0;
this.list = [];
}
let p1 = new Person();
p1.list.push(1);
p1.list; // [1]
let p2 = new Person();
p2.list.push(2);
p2.list; // [2]
定义在 prototype 上的属性和方法,是每个实例所共有的:
function Person() {
this.age = 0;
}
Person.prototype.list = [];
let p1 = new Person();
p1.list.push(1);
p1.list; // [1]
let p2 = new Person();
p2.list.push(2);
p2.list; // [1,2]
前面 1~5 节总结一下!!划重点!!!
- 函数/对象上都有什么呢?
- 对象都有
__proto__
属性 - 函数也有
__proto__
属性,还有原型对象prototype
- 原型对象
prototype
上也有__proto__
属性、还有constructor
属性、以及一些共享属性和方法(挂在 prototype 上)
-
__proto__
是浏览器实现的查看原型方案,也写成[[prototype]]
。__proto__
属性的作用就是,当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的那个对象(父对象)里找,一直找,直到__proto__
属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过__proto__
属性将对象连接起来的这条链路即我们所谓的原型链。 -
constructor
属性其实就是一个拿来保存自己构造函数引用的属性。实例对象的constructor
属性指向构造函数本身。 -
原型对象 prototype
的作用就是,让该函数所实例化的对象们都可以找到公用的属性和方法
第二部分 new 操作符到底做了什么?
new 操作符原理解析
结合前面原理的分析,我们来看下 new
关键字都实现了哪些功能呢?
function Person(age) {
this.age = age;
}
let p = new Person(20);
p; // Person {age:20}
- 首先创建了一个空对象 p = {}
- 然后将 p 的
__proto__
属性指向其构造函数 Person 的原型对象prototype
- 将构造函数内部的
this
绑定到新对象 p 上面,执行构造函数Person()
(其实和调动普通函数一样,并传值this.age = 20
) - 若构造函数返回的是非引用类型,则返回该新建的对象 p;否则返回引用类型的值。
手写一个 new 的实现
不会手写的原理,不是真的理解了的原理,下面我们开始手动实现~
function Person(name, age) {
this.name = name;
this.age = age;
// 构造函数本身也可能有返回结果
// return {
// name,
// age
// }
}
function _new(Func, ...rest) {
// 1. 定义一个实例对象
let p = {};
// 2. 手动将实例中的__proto__属性指向相应原型对象
// 此时 p.constructor 就指向了 Person函数,即 p 已经承认Person函数是它自己的构造函数
p.__proto__ = Person.prototype;
// 3. p 需要能够调用构造函数私有属性/方法
// 也就是需要在实例对象的执行环境内调用构造函数,添加构造函数设置的私有属性/方法
let res = Person.apply(p, arguments);
// 4. 如果构造函数内返回的是对象,则直接返回原返回结果(和直接调用函数一样);否则返回新对象。
return res instanceof Object ? res : obj;
}
let p = _new(Person, "张三", "20");
p; // Person {name: "test"}
p.constructor; // ƒ Person() {}
上述第 3 步,其实构造函数是一种特殊的方法,主要用来在创建对象时初始化对象,即为对象成员变量赋初始值。 函数声明后函数体内的语句并不会立即执行,而是在真正调用时才执行。如果不调用的话,此时 this 没有指向,age 也是没有值的。通过 apply 方法完成了函数调用,并为自己的对象成员变量赋初始值,同时将 this 的指向绑定到实例对象 p 上面了。
第三部分 原型链继承
ES6 的 Class 可以通过 extends 关键字实现继承,这比ES5 的通过修改原型链实现继承,实际上要清晰和方便很多。 但文本主要分析在 Class 出现之前,JS 是如何实现继承的。在后续系列文章中会针对 ES6 中的 Class 继承再进一步扩展。
ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this)
)。
1. 使用 call 方法实现继承
function Father() {
this.id = "1999";
}
function Son(age) {
this.age = age;
// 通过调用父函数的 call 方法来实现继承
Father.call(this);
}
let p = new Son(20);
p; // Son {age: 20, id: "1999"} 拥有了父级的私有属性
p.id; // 1999
说明: 实例化一个对象 p,实现了两步:Son 内 this 指向 p,Father this 指向 Son。所以 p 有了 age、id 属性。
2. 原型链继承
function Father(id) {
this.id = id;
}
function Son(age) {
this.age = age;
}
// Son函数改变自己的prototype指向
// 实际上是Son.prototype.__proto__ 指向了 Father的实例,而 Father的实例指向Father的原型对象。
// 即 Son.prototype.__proto__ == Father.prototype(改变了原型链流向), 看图。。。
Son.prototype = new Father("1999");
let p = new Son(20);
p; // Son {age: 20} 和第一种方式不同,p上面虽没有父级的私有属性id,但是却能访问到
p.id; // 1999
p.__proto__; // Father {id: "1999"}
p.hasOwnProperty("age"); // true
p.hasOwnProperty("id"); // false
不知道你是否也有这样的疑惑,为什么不是写成 p.__proto__.id()
而是 p.id
就能获取到呢? 这就是原型链的查找过程了:
-
实例对象直接查找
p.id
--> 发现没有该属性 --> 通过__proto__
属性去创建它的构造函数的Son.prototype
对象上查找 -->Son.prototype
指向了 Father 的实例对象,有前文 new 方法原理可知,Father 实例上有私有属性id
, 所以p.id
通过原型链查找到了 id 属性,而不用写成p.__proto__.id()
的形式。 -
同理,实例对象直接查找
p.age
--> 当实例对象没有某一属性 --> 通过__proto__
属性去创建它的构造函数的prototype
对象上查找 -->prototype
对象上没有的话,prototype
对象本身也有一个__proto__
属性指向它自己的原型对象(目前是内置 Object 对象),Object 对象的prototype
上面有着构造函数留下的共享属性和方法。比如hasOwnProperty()、valueof()等
。 -
__proto__
属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__
属性所指向的那个对象(父对象)里找,一直找,直到__proto__
属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过__proto__
属性将对象连接起来的这条链路即我们所谓的原型链。
知识点基本就到这里了!!
总结
本文从三部分分析了原型链的基本原理、new 关键字是如何实现的、以及 ES5 中的继承实现方式。
那问题来了,ES6 做了哪些改变? Class 原理、extends 继承原理、语法糖怎么实现的呢,有了本文的基础,再理解后面的这些概念希望能够得心应手,真正理解 JS 这些核心的机制。
参考
继承与原型链
对象原型
用自己的方式(图)理解 constructor、prototype、proto和原型链 (本文图片均参考自这篇文章~)
帮你彻底搞懂 JS 中的 prototype、proto与 constructor(图解)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!