深入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
*/

我们借用构造函数不仅实现了继承,而且还解决了利用原型链实现继承所带来的弊端
但是借用构造函数也是有弊端的

  1. Person函数至少被调用了两次(1次是在创建子类原型的时候,另一次是在创建子类实例的时候)
  2. stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要

我们将借用构造函数实现继承的方式也叫做组合式继承,他虽然不是很完美,但是已经基本没有问题了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值