深入JS中的面向对象(二)
认识JS中的原型
JS中的每一个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另外一个对象。
这个对象有什么用呢?
- 当我们通过引用对象的属性key来获取一个value的时候,它会出发[[Get]]操作
- 这个操作会先检查该对象中是否有对应的属性,如果有的话就使用它
- 如果该对象中没有对应的属性,那么会访问对象中的[[prototype]]内置属性指向的对象上的属性
那么我们该如何获取这个内置属性呢?
获取的方式有两种
方式一:通过对象的__ proto __属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题)
方式二:通过Object.getPrototypeOf方法可以获取到
// 我们每个对象中都有一个 [[prototype]], 这个属性可以称之为对象的原型
var obj = { name: "aaa" } // [[prototype]]
// 给对象中提供了一个属性, 可以让我们查看一下这个原型对象(浏览器提供)
// console.log(obj.__proto__) // {}
// // ES5之后提供的Object.getPrototypeOf
// console.log(Object.getPrototypeOf(obj))
知道上面这个东西对于我们探讨构造函数创建对象是十分有用的
我们要引进一个新的概念,所有的函数都有一个prototype的属性
function foo() {
}
// 函数也是一个对象
// console.log(foo.__proto__) // 函数作为对象来说, 它也是有[[prototype]] 隐式原型
// 函数它因为是一个函数, 所以它还会多出来一个显示原型属性: prototype
console.log(foo.prototype)
var f1 = new foo()
var f2 = new foo()
console.log(f1.__proto__ === foo.prototype) // true
console.log(f2.__proto__ === foo.prototype) // true
再看new操作符
我们知道new操作符操作后会在内存创建出一个新的对象,这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
这就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
function Person() {
}
var p1 = new Person()
var p2 = new Person()
// 都是为true
// console.log(p1.__proto__ === Person.prototype)
// console.log(p2.__proto__ === Person.prototype)
p1.name = "why"
p1.__proto__.name = "kobe"
Person.prototype.name = "james"
p2.__proto__.name = "curry"
console.log(p1.name) // why 如果把p1.name="why"这行注释掉,那么就会打印curry
事实上原型对象上都有一个属性:constructor
这个constructor指向当前的函数对象
function Person(){
}
var p1 = new Person()
var p2 = new Person()
console.log(Person.prototype.constructor) // Person() {}
console.log(p1.__proto__.constructor) // ƒ Person() {}
console.log(p2.__proto__.constructor.name) // Person
当我们需要在原型上添加过多的属性的时候,通常我们会重新弄个原型对象
function Person(){
}
Person.prototype = {
name:"aaa",
age:'18',
eating:function(){
console.log(this.name + '在吃饭')
}
}
console.log(Person.prototype.constructor); // ƒ Object() { [native code] }
我们每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获取constructor属性
而这里我们相当于给prototype重新赋值了一个对象,那么这个新对象的constructor属性就会指向Object构造函数,而不是Person构造函数了
如果我们想要constructor指向Person,那么我们可以手动添加
function Person(){
}
Person.prototype = {
constructor:Person,
name:"aaa",
age:'18',
eating:function(){
console.log(this.name + '在吃饭')
}
}
console.log(Person.prototype.constructor) // Person() {}
但是这样做的话会造成constructor的[[Enumerable]]属性被设置为了true
默认情况下原生的constructor属性是不可枚举的
如果希望解决这个问题,我们可以使用Object.defineProperty()函数
Object.defineProperty(Person.prototype,'constructor',{
enumerable:false,
value:Person
})
在上一篇文章中我们有提到,如果使用构造函数创建对象的话有个弊端,会创建重复的函数浪费内存
现在我们学了原型,就可以做到让所有对象去共享这些函数
我们只需要把这些重复的函数放到Person.prototype的对象上即可
function Person(name,age,height,address){
this.name = name
this.age = age
this.height = height
this.address = address
}
Person.prototype.eating = function () {
console.log(this.name + '在吃饭')
}
Person.prototype.running = funciton () {
console.log(this.name + '在跑步')
}
,
var p1 = new Person('aaa',18,1.68,'福建')
var p2 = new Person('bbb',12,1.71,'广州')
p1.eating() // aaa在吃饭
p2.running() // bbb在跑步
JS中的类和对象
当我们编写代码的时候,我们应该如何来称呼这个Person呢?
在JS中Person应该是一个构造函数
但是从很多面向对象的编程语言过来的开发者,也习惯称它为类,因为类可以帮我们创建出来对象p1,p2
所以从面向对象的编程范式角度来看,Person确实可以称之为类
面向对象的特性
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
- 继承:继承在面向对象中是非常重要的,不仅可以减少重复代码的数量,也是多态的前提
- 多态:不同的对象在执行时表现出不同的形态
这里我们核心来讲讲继承,想要知道在JS中如何实现继承,那么我们就要先了解一下JS中的原型链
我们知道,当从一个对象上获取属性的时候,如果在当前对象上找不到,就回到他的原型上去查找
var obj = {
name: "why",
age: 18
}
obj.__proto__ = {
}
obj.__proto__.__proto__= {
}
obj.__proto__.__proto__.__proto__ = {
address: "北京市"
}
console.log(obj.address) //北京市
查找属性的过程图:
那么什么地方是原型链的尽头呢?比如第三个对象是否也有原型__ photo __属性呢?
console.log(obj.__proto__.__proto__.__proto__) // [Object:null prototype]{}
所以当打印出来是null的时候,就代表这个原型已经是最顶层的原型了
最顶层的原型比其他原型特别的地方在于两个地方
1.它的原型指向null
2.该对象上有很多默认的属性和方法(toString,valueOf等)
并且我们也可以得知,原型链最顶层的对象就是Object的原型对象
现在我们来试试用原型链实现基层
// 1.定义父类的构造函数
function Person() {
this.name = "why"
this.friends = []
}
// 2.在父类的原型上添加方法
Person.prototype.running = function () {
console.log(this.name + 'running')
}
// 3.定义子类的构造函数
function Student(){
this.sno = 111
}
// 4.创建父类对象,并作为子类的原型对象
var p = new Person()
Student.prototype = p
// 5.在子类原型上添加内容
Student.prototype.studying = function(){
console.log(this.name+'studying')
}
// 6.创建子类对象
var stu = new Student()
console.log(stu) // Student sno:111 [[prototype]]:Person
console.log(stu.name) // why
从上面这个例子可以看出我们已经用Student继承了Person
但是这么做有弊端
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
console.log(stu.name) // why
// 2.第二个弊端: ,这个属性会被多个对象共享,如果这个属性是一个引用类型,那么就会造成问题;
// 创建出来两个stu的对象
var stu1 = new Student();
var stu2 = new Student();
// 直接修改对象上的属性, 是给本对象添加了一个新属性
stu1.name = "kobe";
console.log(stu2.name); //why
// 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("kobe");
console.log(stu1.friends); // ['kobe']
console.log(stu2.friends); // ['kobe']
// 3.第三个弊端: 在前面实现类的过程中都没传递参数,因为对象是一次性创建的,没法定制化
var stu3 = new Student("lilei", 112);
console.log(stu3); // Student {sno: 111}
所以为了解决原型链继承中存在的问题,开发人员提供了一种新的思路,借用构造函数实现继承
借用构造函数实现继承的方法非常简单:在子类型构造函数的内部调用父类型构造函数
// 父类: 公共属性和方法
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends)
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
var stu = new Student("why", 18, ["kobe"], 111)
// 原型链实现继承已经解决的弊端
// 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
console.log(stu)
// 结果
/*
Student {name: 'why', age: 18, friends: Array(1), sno: 111}
age: 18
friends: ['kobe']
name: "why"
sno: 111
[[Prototype]]: Person
*/
// 2.第二个弊端: 创建出来两个stu的对象
var stu1 = new Student("why", 18, ["lilei"], 111)
var stu2 = new Student("kobe", 30, ["james"], 112)
// // 直接修改对象上的属性, 是给本对象添加了一个新属性
// stu1.name = "kobe"
// console.log(stu2.name) //kobe
// // 获取引用, 修改引用中的值, 会相互影响
stu1.friends.push("lucy")
console.log(stu1.friends) // ['lilei', 'lucy']
console.log(stu2.friends) // ['james']
// // 3.第三个弊端: 在前面实现类的过程中都没有传递参数
var stu3 = new Student("lilei", 112)
console.log(stu3)
// 结果为
/*
Student {name: 'lilei', age: 112, friends: undefined, sno: 111}
age: 112
friends: undefined
name: "lilei"
sno: 111
[[Prototype]]: Person
*/
我们借用构造函数不仅实现了继承,而且还解决了利用原型链实现继承所带来的弊端
但是借用构造函数也是有弊端的
- Person函数至少被调用了两次(1次是在创建子类原型的时候,另一次是在创建子类实例的时候)
- stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要
我们将借用构造函数实现继承的方式也叫做组合式继承,他虽然不是很完美,但是已经基本没有问题了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程