最近学习了原型和原型链,来做一次总结加深理解,同时也沿着自己的思路看看是否有误,如有表述不当或者错误的地方,欢迎指正,欢迎交流😊
以下笔者先按原型以及原型相关的属性来解释原型和原型链,再讲述他们之间的关系。
原型的设计思想
由于之前一直学习的是强类型的语言,对类和对象有一个固定的理解,当学习JavaScript的时候以为js的类和对象机制也同其他强类型的语言相同,深入学习后发现其实不然。
Brendan Eich在开发JavaScript的时候其实考虑了是否采用完整的面向对象的思想(类&继承),但考虑到JavaScript作为一种简易的脚本语言,并且为了降低学习门槛。
由原型的构造函数来生成实例对象,构造函数可以初始化实例对象的属性和方法,而各实例对象继承原型的属性和方法(公有:值相同,是同一个属性和方法),但各实例对象占据独立的内存,其属性和方法私有(”私有“:值可以相同,但不是同一个属性和方法)
为了便于表述,笔者用私有来表达实例对象各自拥有的属性和方法是不合适的,私有应为外部不能直接访问,需要通过公有方法来访问。
继承同理,继承应为类与对象的关系所拥有
1. prototype
1.1 基本概念
每个构造函数(约定俗成函数名大写的函数为构造函数,与普通函数无异)都具有一个prototype属性,所有实例对象的公有属性和方法,都存放在prototype所指向的对象中——我们称之为原型,而那些私有的属性和方法由构造函数来生成
实例对象被创建(new)后,自动继承原型对象所拥有的属性和方法
我们创建一个构造函数,来查看他的属性和方法
function Person() { }
console.dir(Person);
1.2 继承关系
-
公有属性和方法
实例对象继承原型对象的属性
Person.prototype.name = '小明' Person.prototype.say = function() { console.log('hahahah'); } function Person() {} var person = new Person() console.log(person.name); // 小明 console.log(person.say()); // hahahah如果构造函数拥有该属性,则不会向上查找该属性,同时,修改构造函数属性和方法不会影响原型的属性和方法
Person.prototype.name = '原型对象的小明' Person.prototype.type = 'student' function Person() { this.name = '实例对象的小明' } var person = new Person() person.type = 'worker' console.log(person.name); // 实例对象的小明 console.log(person);// Person {name: "实例对象的小明", type: "worker"} console.log(person.type); // worker构造函数初始化实例对象的属性
function Person(name) { this.name = name } var person = new Person('自定义名字:小明') console.log(person.name); // 自定义名字:小明 -
私有属性和方法
修改实例对象自身的属性不影响其他实例对象的属性
Person.prototype.name = '小明' function Person() { this.phone = 'iphone11' } var person1 = new Person() var person2 = new Person() person1.phone = '小米' console.log(person1.phone); // 小米 console.log(person2.phone); // iphone11
1.3 作用
-
访问构造函数的原型对象
-
提取公有属性和方法
// 提取公有属性 Car.prototype.name = 'BBMW' Car.prototype.lang = 4900 Car.prototype.height = 1400 function Car(color, owner) { this.color = color this.owner = owner // 重复,耦合度高,代码复用性低 // this.name = 'BMW' // this.lang = 4900 // this.height = 1400 } var car = new Car('red', 'tim') var car1 = new Car('green', 'sandy') console.log(car.name, car1.lang); -
操作原型链中的增删改查(接下来会解释)
2. constructor属性
1.1 基本概念
-
实例对象和实例对象的原型对象可以通过constructor来访问构造函数
function Person() {} var person = new Person() console.log(person.constructor); // ƒ Person() {} console.log(Person.prototype.constructor);// ƒ Person() {} console.log(person.constructor === Person.prototype.constructor); // true
但是注意❗:这里的访问是指可以打印出构造函数,但不能通过constructor属性来访问构造函数内部的属性和方法,我们只能通过实例对象来访问构造函数内部的属性和方法
function Person() {
this.name = 'angel'
}
var person = new Person()
console.log(person.name); // angel
console.log(person.constructor.name); // Person
console.log(Person.prototype.constructor.name);// Person
console.log(person.constructor === Person.prototype.constructor); // true
-
改变constructor的作用
给constructor赋值的时候,constructor变成了普通的属性
Person.prototype.constructor = 'male' function Person() { this.constructor = 'female' } var person = new Person() console.log(person.constructor); // female console.log(Person.prototype.constructor);// male console.log(person.constructor === Person.prototype.constructor); // false
1.2 拓展
普通对象也具有constructor属性
var obj = {
name: 'jack'
}
console.log(obj.constructor === Object); // true
从上面的运行结果得知用对象字面量创建的对象是由Object构造函数来创建的
var obj = {}
// 两者相同
var obj = new Object()
3. __proto__属性
所有对象(构造函数也是对象)都具有__proto__属性,我们称之为对象原型,更确切来说是隐式原型,同prototype一样可以访问的原型,区别在于prototype是构造函数所拥有的属性,而__proto__是所有对象都拥有的属性
1.1 new操作符的执行过程
function Person() {}
var person = new Person()
当我们实例化对象的时,在构造函数内部发生了如下操作:
function Person() {
// 当构造函数发生new操作时,先在构造函数内部创建了一个空对象
// 使__proto__指向构造函数的原型
// 返回这个对象
var this = {
__proto__: Person.prototype
}
return this
}
var person = new Person()
所以当我们打印 console.log(person.__proto__ === Person.prototype);
结果为true
回到2.1.2 我们可以推断出
var obj = {
name: 'jack'
}
console.log(obj.constructor === Object); // true
console.log(obj.__proto__ === Object.prototype);// true
1.2 修改原型
构造函数的原型对象和实例的原型对象是相同的,那么我们也可以为他们指定不同的原型
function Person() { }
var person = new Person()
var obj = {
name: '小明'
}
person.__proto__ = obj
console.log(person.__proto__); // {name: "小明"}
console.log(Person.prototype );// {type: "male", constructor: ƒ}
此时person实例对象的原型是对象obj,再通过Person() new出来一个新的实例对象与person实例对象也不再产生联系
如果修改Person.prototype,则保留了原型链的完整性,new出来的所有实例对象都会继承obj
Person.prototype.type = 'male'
function Person() { }
var obj = {
name: '小明'
}
Person.prototype = obj
var person = new Person()
console.log(person); // 其原型具有name属性
console.log(person.__proto__); // {name: "小明"}
console.log(Person.prototype); // {name: "小明"}
var person1 = new Person()
console.log(person1); // 其原型具有name属性
4. 原型链
从上面的分析我们可以得出每个构造函数都有prototype显示指向原型对象,而原型对象可以通过constructor指向构造函数,构造函数的实例又可以通过__proto__隐式指向原型对象
那么,如果让原型对象指向另一种构造函数的实例,就会使得该原型对象指向另一种原型对象,就像一条链子一样把原型关系串起来,我们就称其为原型链
Adult.prototype.work = 'musician'
function Adult() { }
Teenager.prototype = new Adult()
function Teenager() {
this.hobbit = {
music: 'rock'
}
}
Child.prototype = new Teenager()
function Child() {
this.name = 'tim'
}
var child = new Child()
console.log(child.__proto__); // Adult {hobbit: {…}}
console.log(child.__proto__.__proto__); // Adult {}
console.log(child.__proto__.__proto__.__proto__);// {work: "musician", constructor: ƒ}
console.log(child.__proto__.__proto__.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
他们的关系如下:
1.1 在原型链中的增删改查
-
查:逐级查询
function Teenager() { this.name = 'timy' this.hobbit = { music: 'rock' } } Child.prototype = new Teenager() function Child() { this.name = 'tim' } var child = new Child() console.log(child.name); // tim 如果构造函数没有则往上查询 -
删:实例对象可以删除自身的属性,但不可以删除原型对象的属性
Teenager.prototype.type = 'student' function Teenager() { this.name = 'timy' this.hobbit = { music: 'rock' } } var teenager = new Teenager() Child.prototype = teenager function Child() { this.name = 'tim' } var child = new Child() console.log(Teenager.prototype.type); // student 原型对象的type console.log(child.type); // student 逐级查找type delete child.type console.log(child.type); // student 删除失败 console.log(child); // Child类型的实例对象child 可以查看原型链,在原型链上仍然可以找到type删除原型链的属性和构造函数的属性:
delete Teenager.prototype.type console.log(child.type);// undefined 本应该报错,但是child实例对象在查找不到该属性时会自动生成一个type属性,值为undefined delete child.name // 删除实例对象的name属性 console.log(child.name); // timy 逐级查找name delete Child.prototype.name // 删除实例对象的构造函数的name属性 console.log(child); // 查看原型链,无name属性 -
增&改:
Teenager.prototype.type = 'student' Teenager.prototype.name = 'mike' function Teenager() { this.name = 'timy' this.hobbit = { music: 'rock' } } var teenager = new Teenager() Child.prototype = teenager function Child() { } var child = new Child() child.age = 18 // 增加实例对象的name属性 Child.prototype.name = 'anna' // 修改构造函数的name Teenager.prototype.type = 'worker' // 修改原型对象的type Child.prototype.__proto__.type = 'free-worker'// 修改原型对象的type console.log(child.name); // anna 如果没有设置则打印timy console.log(child.age); // 18 添加在实例对象上的属性,原型对象不会添加该属性 console.log(child.type); // free-worker (最终修改的结果)
1.2 实例可以修改(增加或更改)原型的属性吗?
答案是可以修改引用类型
Teenager.prototype.type = 'student'
Teenager.prototype.name = 'mike'
function Teenager() {
this.name = 'timy'
this.hobbit = {
music: 'rock'
}
}
var teenager = new Teenager()
Child.prototype = teenager
function Child() { }
var child = new Child()
child.hobbit.sport = 'basketball' // hobiit是引用类型,可以增加属性
child.hobbit.music = 'pop' // hobiit是引用类型,可以修改属性
console.log(child);
打印结果:
5. 基本类型和函数的原型
初次学习时我思考,既然对象具有__proto__隐式原型,那么构造函数也是对象,它具有__proto__隐式原型吗?
验证一下
function Person() {}
console.log(Person.__proto__); // [Function]
我们到控制台打印以下发现是一个空函数
所有基本类型的构造函数和函数都指向
Function.prototype,它是一个空函数(Empty function)Number.__proto__ === Function.prototype // true Boolean.__proto__ === Function.prototype // true String.__proto__ === Function.prototype // true Object.__proto__ === Function.prototype // true Function.__proto__ === Function.prototype // true Array.__proto__ === Function.prototype // true RegExp.__proto__ === Function.prototype // true Error.__proto__ === Function.prototype // true Date.__proto__ === Function.prototype // true
如果我们继续溯源,Function.prototype的__proto__又是谁呢?
console.log(Function.prototype.__proto__)
结果
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
其实就是Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype) // true
那Object.prototype的__proto__又是谁呢?是null,到达原型链顶层。
6. 图解概括原型链
经过上面的所有解释,想必已经对原型和原型链有一个清晰的了解了
再来看看这张图,涵盖了我所讲的内容