Js 面向对象-构造函数与原型
上篇学了 js 中对象的基本概念和实现原理, 然后重点是掌握对象 属性描述符, 包含存取属性, 数据属性, 即通过 Object.defineProperty(obj, prop, desc) 来实现对对象属性的增删改查控制, 具体表现为 configurable, enumerable 配合 value + writable 或者 配合 get() + set() 的组合来实现各种高级操作.
本篇则开始讲 js 的对象如何批量生成, 即 new 一个对象, 但 js 中是没有 class 的, 只有函数, 它正是通过对函数, 原型, 等进行拓展, 逐步来实现类似 java / c++ 中的面向对象效果的, 这也是 js 这门语言最为精妙和独特的地方.
批量创建对方式
之前都是用字面量, 或者 new 一个 object, 但当若我们要创建多个对象, 显然通过去一个个写 字面量 的方式是不行的, 这里引入一个设计模式叫 工厂模式.
工厂模式
// 工厂模式: 工厂函数
function createPerson(name, age, height, city) {
}
// 创建很多个对象, 同时要保证每个对象数据是互隔离的
var p1 = createPerson('youge', '20', 1.8, '长安镇')
var p2 = createPerson('cj', '18', 1.7, '虎门镇')
要实现这个工厂函数, 则在里面先创建一个空对象, 然后给它逐个添加上属性, 最后返回该对象就好了.
// 工厂模式: 工厂函数
function createPerson(name, age, height, city) {
var p = {}
p.name = name
p.age = age
p.height = height
p.city = city
// 还能添加方法
p.eating = function() {
console.log(this.name + ' is eating')
}
p.running = function() {
console.log(this.name + ' is running')
}
return p
}
// 创建很多个对象, 同时要保证每个对象数据是互隔离的
var p1 = createPerson('youge', '20', 1.8, '长安镇')
var p2 = createPerson('cj', '18', 1.7, '虎门镇')
var p3 = createPerson('yaya', '18', 1.6, '长安镇')
看上去好像是可以了, 但这样的方式还是有很大缺点的. 首先是对象类型的缺失.
console.log(typeof p1) // 是 object, 而非 Person
cosole.log(p1);
{
name: 'youge',
age: '20',
height: 1.8,
city: '长安镇',
eating: [Function (anonymous)],
running: [Function (anonymous)]
}
没有类型就很麻烦, 这样我们通过不同的工厂函数, 创建出来的对象的类型, 都是 object 就没法区分了.
构造函数方式
构造函数也称为 构造器 (constructor), 通常是我们创建对象时会自动调用的函数. 在其他编程语言里, 构造函数是存在于类中的一个方法, 称为 构造方法.
先来看看几个其他编程语言的实现, 先看 Java:
// 1. 方法名与类名必须一致
// 2. 没有返回类型 (void)也不能写
// 3. 使用 new 关键字创建时自动调用
public class Person {
private String name;
private age int;
// 无参构造器
public Person() {
this.name = 'youge';
this.age = 18;
}
// 有参构造器
public Person(String name, int age) {
this.name = name;
thi.age = age
}
// Getter, Setter ... 啰嗦一大堆
}
// 使用
Person p1 = new Person() // 无参
Person p2 = new Person('youge', 18) // 有参
然后来看下 Python:
class Person:
def __init__(self, name="youge", age=18):
self.name = name
self.age = age
# 使用, new 都不用写, 优雅呀
p1 = Person()
p2 = Person('cj', 30)
再来看一下 Go 语言, 注意它也是没有 class 的, 清新脱俗.
// go 没有类, 有结构体, 也很优雅
type Person struct {
Name string,
Age int
}
// 构造函数
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age
}
}
// 普通方法
func (p *Person) Eating() {
fmt.Println(p.name, ' is eating')
}
// 使用
func main() {
p := newPerson("youge", 30)
p.Eating()
}
回到 js 这里来, 它的构造函数 就是一个普通函数, 从表现形式上看. 区别在于, js 内置了一个操作符 new,
如果一个普通的 js 函数被 new 操作符来调用, 则这个函数被称为一个构造函数.
// js 函数调用
function foo() {
console.log('foo 函数被调用啦')
}
// 1. 普通调用
foo()
// 2. 显示调用 call, apply, bind
foo.call('nb')
// 3. new 调用, 会返回一个对象
var p = new foo() // foo ...
console.log(p) // foo {}
| 调用方式 | 语法 | this 指向 |
典型用途 |
|---|---|---|---|
| 普通函数调用 | func() |
全局对象 / undefined |
工具函数 |
| 方法调用 | obj.method() |
调用者对象 (obj) |
对象行为 |
| 构造函数调用 | new Func() |
新创建的实例 | 创建对象 |
| 显式绑定调用 | func.call/apply/bind() |
指定的对象 | 控制 this |
new 操作符的作用
如果一个函数被 new 操作符调用, 则内部会进行 4步操作:
- 内存中创建一个新对象 (空对象)
- 对象内部的 [[prototype]] 属性会被赋值为该 构造函数的 prototype 属性
- 执行构造函数的代码
- 返回创建的对象, 若构造函数显示返回别的的对象, 则优先
这里比较难理解的是第二步, 先看结论吧, 当做逻辑学的东西看, 慢慢就懂了:
- js 中, 每个对象都有一个内置的
[[prototype]]属性, 是个指针, 指向该对象的原型对象, 也是一个对象 - js 中, 函数是一个特殊的对象, 前面可以通过 foo.call() 显示调用, 这就是因为 foo 既是函数, 也是对象的缘故
- js 中, 每个函数都有一个 显示的 prototype 属性, 是个指针, 也是一个对象
因此, js 函数具有二义性, 既是函数, 也是对象, 但注意:
function foo() {}
// foo 此时变为构造函数
var obj = new foo() // () 不加也行的
new 最为核心的一步内置操作是:
// [[prototype]] 可以简单用 对象.__proto__ 访问
obj.__proto__ === foo.prototype
为了区分一个函数是作为构造函数来用的, 我们有个 约定大于配置 的规范, 函数首字母大写啦~
// 约定规范: 构造函数的首字母大写, 小写也行其实
function Person(name, age, height, city) {
// 属性
this.name = name
this.age = age
this.height = height
this.city = city
// 方法
this.eating = function() {
console.log(this.name + ' is eating')
}
}
var p1 = new Person('cj', 30, 1.8, '长安镇')
var p2 = new Person('youge', 18, 1.7, '虎门镇')
console.log(p1.name) // cj
p2.eating() // youge is eating
通过这样的构造函数方式来创建对象, 最大的缺点是, 对于 方法 来说, 每创建一个对象, 都会重新给这个对象创建一次方法, 这样对象创建越多, 则越是消耗性能的, 而且有点重复浪费.
// 这个 eating() 方法会被每个对象重新创建, 但它的功能是一样的
console.log(p1.eating === p2.eating) // false
Js 原型
在 js 中每个对象都有一个内置属性 [[prototype] 是一个 指针 /引用, 它可以指向另外一个对象, 被指向的对象则被称为该对象的 原型 prototype.
原型有点类似于 Java / Python 等语言中的 "父类", 用来说明当前对象, 是 "继承" 谁
[[Prototype]] 指针, 早期不能访问, 但浏览器实现了
__ proto__属性, 可以指向原型对象
对象原型 (隐式) [[prototype]]
// 对象 的 原型对象
var obj = { name: 'youge'}
var obj2 = {}
// obj2 = { __proto__: {}}
// 查看原型对象
console.log(obj.__proto__) // [Object: null prototype] {}
console.log(obj2.__proto__) // [Object: null prototype] {}
// 这两个原型对象, 是同一个原型对象
console.log(obj.__proto__ === obj2.__proto__)
// __ proto__ 有兼容性, Object.getPrototypeOf() 通用
console.log(Object.getPrototypeOf(obj) === obj.__proto__) // true
// 原型的作用
// 获取属性时候, 会触发 [[get]] 操作
// 1. 先从当前对象中去找属性, 找到则直接使用
// 2. 若没有找到, 则会沿着它的原型去找 [[prototype]]
// console.log(obj.age) // 找不到 undifined
// 添加到原型里面就能可以了
obj.__proto__.age = 18
console.log(obj.age) // 18
// ob2 也能访问
console.log(obj2.age) // 18
函数原型 (显式) prototype
函数也是一个对象, 且有一个显示的 prototype 属性( 除箭头函数), 也是一个 指针 指向另外一个对象.
function foo() {} // 作为构造函数
var f1 = new foo()
var f2 = new foo()
// f1, f2 对象的 [[prototype]] -> 构造函数 foo.prototype
console.log(foo.prototype) // {}
// new 关键字会自动将创建出来的对象的隐式原型, 指向 构造函数的显示原型
console.log(f1.__proto__ === foo.prototype) // true
console.log(f2.__proto__ === foo.prototype) // true
就函数作为构造函数时, 其显示的 prototype 会作为其所有 被 new 出来的对象的 [[prototype]] 对象
对象的 [[prototype]] 用来指向其 "父类" 是谁, 构造函数的 prototype 用来给所有子对象 "共享空间"
foo.prototype !== foo.__ proto __ 的, 前者作为构造函数, "向下共享", 后者作为对象, "指向父类"
因此, js 中的函数具有二义性, 它是一个函数, 也是一个对象, 既有作为对象的隐式 [[prototype]] 对象 (父类), 也有作为构造函数的显式 prototype (给子类共享)
我也是搞了好久才明白这个东西的, 但是请相信, 一旦理解了函数这个特性, 那后面的原型链, 继承等这些东西都轻松拿捏.
// 仅将函数作为对象看
obj.__proto__ === foo.prototype // true
// Funtion() 是 js 内置的函数, 是所有函数的 构造函数
foo.__proto__ === Function.prototype // true
// 那从逻辑推断, Functon() 的构造函数,就是它自身
console.log(Function.__proto__ === Function.prototype) // true
这个设计简直完美, 逻辑完整闭环呀!.
Constructor 属性
在 js 的原型对象上会默认添加一个属性 constructor, 它是一个 函数的引用, 指向当前的构造函数
引用 vs: 指针
从行为上看, 就是
指针, 它指向了一个函数对象, 而已被传值, 调用, 赋值从实现上看, 不是 指针, 是一个高级语言的引用 (reference)
引用更像一个"快捷方式", 知道去哪找到真正的对象, 但是不能看到 "硬盘地址"
// constructor 是确实存在的
function foo() {}
console.log(foo.prototype) // {}, 有隐藏一些属性
// 看全部属性
console.log(Object.getOwnPropertyDescriptors(foo.prototype))
// new 构造函数出来对象 的 原型对象
{
constructor: {
value: [Function: foo],
writable: true,
enumerable: false,
configurable: true
}
}
可以看到它里面的 value, 存的就是其构造函数的引用.
// constructor 引用
function Person() {}
var p1 = new Person()
var p2 = new Person
// p1, p2 的原型对象是 Person.prototype 对象
// Person.prototype 对象有一个 constructor 属性, 是个引用
// 这个引用, 指向 Person 这个构造函数对象
console.log(Person.prototype.constructor === Person)
// 再来分步理解一下
console.log(p1.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(p1.__proto__.constructor == Person) // true
我们是可以直接修改整个 prototype 对象的, 但不建议, 这样会破坏默认的原型链结构啦.
// prototype 实现属性, 方法添加和共享
function Person() {}
// 可以添加自己的属性到 Person.prototype 上, 这样能实现共享
Person.prototype.name = 'youge'
Person.prototype.age = 18
Person.prototype.eating = function() {console.log(this.name + ' is eating')}
var p1 = new Person()
var p2 = new Person()
// 原型上的属性都是共享的
console.log(p1.name) // youge
console.log(p2.name) // youge
p1.eating()
// 修个对象会优先查找, 添加到的是自己对象域
p1.name = 'cj'
console.log(p1.name) // cj
console.log(p2.name) // youge
// 也可以修改整个 prototype 对象, 即指向了新的对象
Person.prototype = { }
console.log(p1.name) // cj
// p2.name 依旧指向 原来的 prototype, 不会改变 !!!
console.log(p2.name) // name
| 对象 | [[Prototype]] 指向 |
|---|---|
p1 |
旧原型对象 { name: 'youge', age: 18 } |
p2 |
旧原型对象 { name: 'youge', age: 18 } |
Person.prototype |
指向新对象 { }(新原型) |
👉 p1 和 p2 仍然指向同一个旧原型对象,所以它们的原型是一致的,没有分裂, 也是被引用不会被垃圾回收.
只有在修改了 Person.prototype 之后再 new 对象 p3 才会指向新对象哈.
// Person.prototype = { }, 前面的 p1, p2 依旧引用之前原型对象
var p3 = new Person // 后创建的引用新的原型对象
console.log(p3.name) // undefined
但是这样粗暴修改了 prototype 对象之后, 它的 constructor 也就没了, 相当于改掉了了 js 内置设置.
console.log(Person.prototype.construtor === Person) // false
要还原一下的话, 通过之前学的 Object.defineProperty() 加上去就行啦.
// 改了 prototype 对象后
console.log(Person.prototype.construtor === Person) // false
// 再将 constructor 属性加回来, 并指向 构造函数自身
Object.defineProperty(Person.prototype, "construtor", {
value: Person,
configurable: true,
writable: false, // 不让修改
enumerable: false // 不让枚举
})
console.log(Person.prototype.construtor === Person) // true
创建对象-构造函数+原型
此前用构造函数创建对象的方式, 有个弊端是, 会创建重复的方法,.
现在学了原型之后, 则可以 将这些重复的方法, 添加到原型对象上, 实现共享.
对象属性通常是不需要的, 因为都是要 "隔离", 直接加自己对象即可, 而共享的更多是方法, 添加的原型中即可
// 创建对象: 构造函数 + 原型
function Person(name, age, height, city) {
// 对象各自的属性
this.name = name
this.age = age
this.height = height
this.city = city
// 对象的各自方法, 会被重复创建的哦
this.eating = function() {
console.log(this.name + ' is eating')
}
}
// 每个实例对象共享的方法, 放在原型上
Person.prototype.runing = function() {
console.log(this.name + ' is runing')
}
var p1 = new Person('youge', 18, 1.88, '长安镇')
var p2 = new Person('cjj', 28, 1.78, '虎门镇')
// 调用自己对象上的方法
p1.eating()
// p1, p2 都有各自的 eating 方法
console.log(p1.eating === p2.eating) // false
// 调用原型上的方法
p1.runing() // youge is runing
p2.runing() // cjj is runing
// p1, p2 共享原型上的 runing 方法, 不用重复被创建, 节省内存
console.log(p1.runing === p2.runing) // true
至此, 关于 js 对象的构造函数, js 函数也是对象, 对象的隐式原型对象, 构造函数对象的显式原型对象等核心概念就理解得差不多了, 对原型概念的理解非常关键, 一旦真正理解, 则后续受益无穷呀.

浙公网安备 33010602011771号