本文深入解析前端面向对象编程的核心机制,涵盖ES5的工厂模式与构造函数,以及ES6的class语法。重点讲解对象属性的封装、函数方法的内存优化,并详细剖析prototype与__proto__构成的原型链访问规则,阐明对象赋值与属性查找的实际行为。
一、面向对象编程
1. 概念
- 面向对象编程是对具有相同属性和函数方法的一类事物进行描述,从而实现特定的代码功能。
- 它通过抽象具有相同属性和函数方法的类来实现代码功能。
2. 理解
首先需要定义一个对象。
利用对象的属性来存储所需的数据。
将程序执行所需的数据全部以属性的形式存储在对象中。
利用函数方法来存储程序的执行代码。
将执行代码所需的程序逻辑进行封装。
以函数方法的形式存储这些代码。
当调用并执行函数方法时。
所使用的数据来源于对象属性中预先存储的数据。
二、语法形式
工厂模式
function 函数(形参,形参...){
const obj= {};
//创建一个空对象
obj.属性 = 属性;
obj.属性 = 属性;
...
设定对象的属性和属性值
obj.函数方法 = function(){}
obj.函数方法 = function(){}
...
设定对象的函数方法
return obj;
返回值是这个创建的对象
}
/*
工厂模式 是 创建 属性和函数方法相同
属性值不同的 对象
创建的对象 都 相同的属性 不同的属性值
还有 完全相同的函数方法
每一个对象都存储了完全相同的函数方法
造成有 冗余的代码程序
*/
ES5语法
function 构造函数(形参,形参...){
//在构造函数中通过this指向设定操作对象的属性
this.属性 = 形参;
this.属性 = 形参;
}
//在构造函数外在构造函数的prototype中定义函数
构造函数.prototype.函数方法 = function(){}
构造函数.prototype.函数方法 = function(){}
//通过new关键词 调用构造函数创建实例化对象
ES6语法
使用class关键词定义构造函数
class 构造函数{
//在构造函数中定义对象的属性属性值
constructor(形参,形参...){
this.属性 = 形参;
this.属性 = 形参;
}
//在构造函数外定义函数方法 js程序自动定义在prototype 中
函数方法(){}
函数方法(){}
}
const 实例化对象 = new 构造函数(实参,实参...);
通过new关键词调用构造函数创建实例化对象
三、批量生产对象
字面量方式
const obj = {}
obj.a = 1
obj.b = 2
console.log(obj)
const obj1 = {}
obj1.a = 1
obj1.b = 2
console.log(obj1)
//不能批量生产
内置构造函数
const obj = new Object()
obj.a = 1
obj.b = 2
console.log(obj)
const obj1 = new Object()
obj1.a = 1
obj1.b = 2
console.log(obj1)
//不能批量生产
工厂函数
function createObj (name, age) {
// 1. 手动创建一个对象
const obj = {}
// 2. 手动向对象内部添加属性
obj.name = name
obj.age = age
// 3. 手动返回一个对象
return obj
}
//这个方法能够创建对象, 并且批量生产也可以
自定义构造函数
function createObj () {
// 1. 自动创建出来一个对象
// 2. 手动向对象添加属性
// 3. 自动返回一个对象
}
const o1 = new createObj() // 这种调用方式, 才是 构造函数
const o2 = createObj() // 这就是一个普通函数的调用
console.log(o1)
console.log(o2)
/*
自定义构造函数, 本质是就是一个函数
当他和 new 关键字连用的时候, 就叫做构造函数
自动创建一个对象
手动向创建的对象添加属性
自动返回一个对象
*/
四、自定义构造函数的书写
1. 构造函数必须与 new 关键字连用。
如果没有和 new 连用,那么它仅仅是一个普通函数。
2. 当一个函数和 new 关键字连用时,该函数被称为自定义构造函数。此时,函数内部的 this 指向返回出来的对象。
3. 构造函数不能使用箭头函数。
因为箭头函数内部没有 this 绑定。
4. 构造函数内部通常不需要 return。
- 如果 return 基本数据类型,写了和没写一样。
- 如果 return 引用数据类型,则构造函数将失效。
5. 书写构造函数时,首字母建议大写。
目的仅仅是为了和普通函数进行区分。
6. 我们构造函数通过 new 关键字创建出来的对象,叫做实例化对象。本质上它还是一个对象,只不过在命名上称为实例化对象(或实例对象)。
我们把构造函数通过 new 关键字创建对象的过程叫实例化。
和普通函数一样,只不过调用的时候要和 new 连用,否则就是一个普通函数调用。
1. 不写 new 的时候就是普通函数调用,没有创造对象的能力。
function Person () {}
var o1 = new Person() // 一个对象
var o2 = Person() // 什么也得不到, 就是一个普通函数的调用
2. 首字母不大写,只要和 new 连用,就有创造对象的能力。
function person () {}
var o1 = new person() // 一个对象
3. 当调用的时候,如果不需要传递参数可以不写 (),但是建议写上。
function person () {}
var o1 = new person // 一个对象
5. 构造函数内部的 this,由于和 new 连用的关系,是指向当前实例对象的。
function Person () {
console.log(this)
}
var o1 = new Person() // this ---> o1
var o2 = new Person() // this ---> o2
五、使用构造函数创建一个对象
function Person() {
this.name = 'Jack'
this.age = 18
}
var o1 = new Person()
var o2 = new Person()
function Person() {
this.name = 'Jack'
this.age = 18
this.sayHi = function () {
console.log('hello word')
}
}
var o1 = new Person()
var o2 = new Person()
// 有缺点
function Person() {
this.name = 'Jack'
this.age = 18
this.sayHi = function () {
console.log('hello word')
}
}
/**
* 第一次 new 的时候, Person 这个函数要执行一遍
* 执行一边就会创造一个新的函数, 并且把函数地址赋值给 this.sayHi
*/
var o1 = new Person()
/**
* 第二次 new 的时候, Person 这个函数要执行一遍
* 执行一遍就会创造一个新的函数, 并且把函数地址赋值给 this.sayHi
*/
var o2 = new Person()
- 我们在使用构造函数的时候,可以通过一些参数和内容来向当前的对象中添加一些内容。
- 我们得到的两个对象里面都有自己的成员 name 和 age。
- 我们在写构造函数的时候其实也可以添加一些方法进去。
这样也是可以的,但是有一点小缺点。
- 第一次 new 的时候,Person 这个函数要执行一遍,执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi。
- 第二次 new 的时候,Person 这个函数要执行一遍,执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi。
- 这样写的话,我们两个对象内的
sayHi函数就是一个代码一模一样、功能一模一样。 - 但却占用了两个内存空间。
- 也就是说
o1.sayHi是一个地址,o2.sayHi是一个地址。 - 所以我们执行
console.log(o1.sayHi === o2.sayHi)得到的结果是false。 - 缺点:一模一样的函数出现了两次,占用了两个空间地址。
- 要想解决的话,就需要一个东西,叫做原型。
原型
- 原型的出现,就是为了解决构造函数的缺点。
- 也就是给我们提供了一个给对象添加函数的方法。
- 不然构造函数只能给对象添加属性,不能合理地添加函数就太 low 了。
prototype
- 每一个函数天生自带一个成员,叫做 prototype,是一个对象空间。
- 既然每一个函数都有,构造函数也是函数,构造函数也有这个对象空间。
- 这个
prototype对象空间可以由函数名来访问。
function Person () {}
console.log(Person.prototype) // 是一个对象
- 既然是一个对象,那么我们就可以向里面放入一些东西。
function Person() {}
Person.prototype.name = 'prototype'
Person.prototype.sayHi = function () {}
- 重点:在函数的 prototype 里存储的内容,不是给函数使用的,是给函数的每一个实例化对象使用的。
__proto__
每一个对象都天生自带一个成员,叫做 __proto__,是一个对象空间。
既然每一个对象都有,实例化对象也是对象,那么每一个实例化对象也有这个成员。
这个 __proto__ 对象空间是给每一个对象使用的。
当你访问一个对象中的成员的时候。
- 如果这个对象自己本身有这个成员,那么就会直接给你结果。
- 如果没有,就回去
__proto__这个对象空间里面找,里面有的话就会有结果。 - 未完待续...
那么这个 __proto__ 又指向哪里呢?
- 这个对象是由那个构造函数 new 出来的。
- 那么这个对象的
__proto__就指向这个构造函数的prototype。
function Person() {}
var p1 = new Person()
console.log(p1.__proto__ === Person.prototype) // true
实例化对象的 __proto__ 和所属构造函数的 prototype 是一个对象空间。
我们可以通过构造函数名称来向 prototype 中添加成员。
对象在访问的时候自己没有,可以自动去自己的 __proto__ 中查找。
那么,我们之前构造函数的缺点就可以解决了。
- 我们可以把函数放在构造函数的
prototype中。 - 实例化对象访问的时候,自己没有,就会自动去
__proto__中找。 - 那么也可以使用了。
function Person() {}
Person.prototype.sayHi = function () {
console.log('hello Person')
}
var p1 = new Person()
p1.sayHi()
p1自己没有sayHi方法,就会去自己的__proto__中查找。p1.__proto__就是Person.prototype。- 我们又向
Person.prototype中添加了sayHi方法。 - 所以
p1.sayHi就可以执行了。 - 到这里,当我们实例化多个对象的时候,每个对象里面都没有方法。
- 都是去所属的构造函数的
prototype中查找。 - 那么每一个对象使用的函数,其实都是同一个函数。
- 那么就解决了我们构造函数的缺点。
function Person() {}
Person.prototype.sayHi = function () {
console.log('hello')
}
var p1 = new Person()
var p2 = new Person()
console.log(p1.sayHi === p2.sayHi)
p1是Person的一个实例。p2是Person的一个实例。- 也就是说
p1.__proto__和p2.__proto__指向的都是Person.prototype。 - 当
p1去调用sayHi方法的时候是去Person.prototype中找。 - 当
p2去调用sayHi方法的时候是去Person.prototype中找。 - 那么两个实例化对象就是找到的一个方法,也是执行的一个方法。
结论
- 当我们写构造函数的时候。
- 属性我们直接写在构造函数体内。
- 方法我们写在原型上。
原型链
- 构造函数的
prototype是一个对象,每一个对象又自带__proto__属性。 - 构造函数的 prototype 里面的 proto 属性又指向哪里?
一个对象所属的构造函数。
每一个对象都有一个自己所属的构造函数。
比如:数组。
// 数组本身也是一个对象
var arr = []
var arr1 = new Array()
- 以上两种方式都是创造一个数组。
- 我们就说数组所属的构造函数就是
Array。
比如:函数。
// 函数本身也是一个对象
var fn = function () {}
var fun = new Function()
- 以上两种方式都是创造一个函数。
- 我们就说函数所属的构造函数就是
Function。
constructor
- 对象的
__proto__里面也有一个成员叫做 constructor。 - 这个属性就是指向当前这个对象所属的构造函数。
链状结构
- 当一个对象我们不知道准确的是谁构造的时候,我们就把他看成
Object的实例化对象。 - 也就是说,我们的 构造函数 的 prototype 的 proto 指向的是 Object.prototype。
Object.prototype也是个对象,那么它的__proto__又指向谁呢?- 因为
Object的 js 中的顶级构造函数,我们有一句话叫 万物皆对象。 - 所以
Object.prototype就到顶了,Object.prototype 的 __proto__就是 null。
原型链的访问原则
- 访问一个对象的成员时,自己没有就会去
__proto__中找。 - 接下来就是,如果
__proto__里没有就再去__proto__里面找。 - 一直找到
Object.prototype里面都没有,那么就会返回undefined。
对象的赋值
到这里,我们就会觉得,如果是赋值的话,那么也会按照原型链的规则来。
但是:并不是!并不是!并不是!重要的事情说三遍。
赋值的时候就是直接给对象本身赋值。
- 如果原先有就是修改。
- 如果原先没有就是添加。
- 不会和
__proto__有关系。
总结
到了这里,我们就发现了面向对象的思想模式。
- 当我想完成一个功能的时候。
- 先看内置构造函数有没有能给我提供一个完整功能对象的能力。
- 如果没有,我们就自己写一个构造函数,能创造出一个完成功能的对象。
- 然后再用我们写的构造函数 new 一个对象出来,帮助我们完成功能就行了。
以上就是前端面向对象编程之ES5语法ES6语法详解的详细内容,更多关于前端面向对象编程ES5ES6语法的资料请关注本站其它相关文章!