内容偏长建议先码后看
什么是对象
- Everything is object(万物皆对象)
- 我们可以从两层次来理解
- 对象是单个事物的抽象
- 对象是一个容器,封装了属性(property)和方法(method)
- 属性:对象的状态
- 方法:对象的行为
- 在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集
- ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数
什么是面向对象
- 面向对象编程 — Object Oriented Programming,简称 OOP,是一种编程开发思想
- 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟
面向对象与面向过程对比
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥的结果
- 面向对象将执行者转变为指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性
- 封装性
- 继承性
- [多态性]抽象
总结
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务
- 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目
体验面向过程和面向对象
// 面向过程
// 定义学生的对象
// var std1 = {name: "Bob", score: 89};
// var std2 = {name: "Mike", score: 98};
// // 封装一个打印学生成绩的函数
// function printScore(student) {
// console.log("姓名:" + student.name + "成绩:" + student.score);
// }
// // 具体调用函数打印出对应成绩
// printScore(std1);
// printScore(std2);
// 面向对象
// 首先考虑的不是整个流程,而是将一个学生当成一个对象,对象有两个属性存储姓名和成绩,并且对象自己有打印成绩的功能,将所有跟学生有关的属性和行为都封装到对象身上
// 在这个过程,我们已知会由多个类似的对象,可以利用构造函数的方法先进行封装,然后创建单独的对象
// 抽象所有的数据行为成一个模板(Class)
function Student(name,score) {
this.name = name;
this.score = score;
this.printScore = function () {
console.log("姓名:" + this.name + "成绩:" + this.score);
};
}
// 根据模板创建具体的实例对象(Instance)
var std1 = new Student("Bob",90);
var std2 = new Student("Mike",98);
// 调用实例对象自己的方法
std1.printScore();
std2.printScore();
面向对象的设计思想
- 抽象出 Class(构造函数)
- 根据 Class(构造函数)创建 Instance(实例)
- 指挥 Instance 得结果
创建对象的几种方式
- new Object() 构造函数
// 简单方式 new Object() var person = new Object(); person.name = "Bob"; person.age = 18; person.sayName = function () { console.log(this.name); }; person.sayName();
- 对象字面量 {}
// 对象字面量化简 var person1 = { name : "Bob", age : 18, sayName : function () { console.log(this.name); } }; var person2 = { name : "Mike", age : 20, sayName : function () { console.log(this.name); } }; person1.sayName(); person2.sayName();
- 工厂函数
// 工厂函数 // function createPerson(name,age) { // // 添加一个新对象 // var person = new Object(); // person.name = name; // person.age = age; // person.sayName = function () { // console.log(this.name); // }; // // 必须有返回值 // return person; // } function createPerson(name,age) { return { name : name, age : age, sayName : function () { console.log(this.name); } }; }; // 生成真正的对象 var person1 = createPerson("John",19); var person2 = createPerson("Mike",18); person1.sayName();
- 自定义构造函数
// 下一模块讲解
构造函数
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 演示 new 的功能
// function Person(name,age) {
// // var instance = new Object();
// // this = instance;
// this.name = name;
// this.age = age;
// this.sayName = function () {
// console.log(this.name);
// };
// // return instance;
// }
// 生成对象实例
var person1 = new Person("Bob", 18);
var person2 = new Person("Mike", 20);
// 调用方法
person1.sayName();
person2.sayName();
// 通过构造函数生成的实例是可以找到自己当初的构造函数的
// var arr = new Array(1, 2);
// console.log(arr);
// constructor 属性,构造器、构造函数
// 每个对象的 constructor 属性值就是生成这个对象的构造函数
// console.log(arr.constructor);
// console.log(person1.constructor);
构造函数和实例对象的关系
- 构造函数是根据具体的事物抽象出来的抽象模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都通过一个 constructor 属性,指向创建该实例的构造函数
- 注意:constructor 是实例的属性的说法不严谨,具体后面的原型会讲到
- 可以通过 constructor 属性判断实例和构造函数之间的关系
- 注意:这种方式不严谨,推荐使用 instanceof 操作符,后面学原型会解释为什么
静态成员和实例成员
- 使用构造函数方法创建对象时,可以给构造函数和创建的实例对象添加属性和方法,这些属性和方法都叫做成员
- 实例成员:在构造函数内部添加给 this 的成员,属于实例对象的成员,在创建实例对象后必须有对象调用。
- 静态成员:添加给构造函数自身的成员,只能使用构造函数调用,不能使用生成的实例对象调用。
// 自定义构造函数
function Person(name,age) {
// 实例成员 ,通过将来生成的实例对象进行调用的成员
// 创建时,是直接添加给函数内部的 this
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
// 静态成员 -- 直接给构造函数添加的成员
Person.version = "1.0";
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 调用实例成员
// console.log(person1.name);
// 使用构造函数调用实例成员会出错
// console.log(Person.name);
// Person.sayName();
// 调用静态成员,只能通过构造函数进行调用
// console.log(Person.version);
// console.log(person1.version);
构造函数的问题
- 浪费内存
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
// this 内部的 type 属性值是不变的
this.type = "human";
// 每个对象的 sayName 方法也是一样的
this.sayName = function () {
console.log(this.name);
};
}
// 判断各自的方法是否是同一个函数
console.log(person1.sayName === person2.sayName);
解决方法1: 将公共的函数提取到构造函数之外
function sayName() {
console.log(this.name);
}
// 问题:如果有多个公共函数,需要在外部创建多个函数,可能会造成命名冲突
function sayAge() {
console.log(this.age);
}
function Person(name,age) {
this.name = name;
this.age = age;
// this 内部的 type 属性值是不变的
this.type = "human";
// 每个对象的 sayName 方法也是一样的
this.sayName = sayName;
this.sayAge = sayAge;
}
console.log(person1.sayName === person2.sayName);
- 问题:如果有多个公共函数,需要在外部创建多个函数,可能会造成命名冲突
解决方法第2种:将多个公共的函数封装到一个对象
var fns = {
sayName : function () {
console.log(this.name);
},
sayAge : function () {
console.log(this.age);
}
};
function Person(name,age) {
this.name = name;
this.age = age;
// this 内部的 type 属性值是不变的
this.type = "human";
// 每个对象的 sayName 方法也是一样的
this.sayName = fns.sayName;
this.sayAge = fns.sayAge;
}
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// person1.sayName();
console.log(person1.sayName === person2.sayName);
console.log(person1.sayAge === person2.sayAge);
原型对象
- 使用原型对象可以更好的解决构造函数的内存浪费问题
prototype 原型对象
- 任何一个函数都具有一个 prototype 属性,该属性是一个对象
- 可以在原型对象上添加属性和方法
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto
- 实例对象可以直接访问原型对象成员
更好的解决方法prototype
- JS规定,没一个构造函数都有一个 prototype 属性,指向构造函数的原型对象。
- 这个原型对象的所有属性和方法,都会被一个构造函数的实例对象所拥有。
- 因此,我们可以把所有对象实例所需要共享的属性和方法直接定义在 prototype 对象上
- 解决内存浪费问题
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 更优的解决方法,将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
Person.prototype.sayName = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.name);
};
Person.prototype.sayAge = function () {
// 方法调用时,哪个对象调用,this 指向的就是谁
console.log(this.age);
}
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 调用原型对象上公用的属性和方法
person2.sayAge();
console.log(person1.sayAge === person2.sayAge);
原型链
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
// this.sayName = function () {
// console.log("hello");
// }
}
// 将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
// Person.prototype.sayName = function () {
// console.log(this.name);
// };
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 方法的调用查找
// person1.sayName();
console.log(person1.valueOf());
// console.log(person1);
// var o = person1.__proto__; //指向的是 Person 构造函数的原型对象
// // 任何一个对象都有 __proto__ 属性,指向的就是该对象的 构造函数的 原型对象
// var o2 = o.__proto__;
// console.log(o2.constructor);
// console.dir(o2);
// console.dir(o2.__proto__);
实例对象读写原型对象成员
原型链查找机制
- 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性:
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
实例对象读写原型对象成员
- 读取:
- 现在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回 undefined
- 值类型成员写入(实例对象.值类型成员 = xx):
- 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉原型对象成员的访问
- 引用类型成员写入(实例对象.引用类型成员 = xx):
- 同上
- 复杂类型成员修改(实际对象.成员.xx = xx):
- 同样会现在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
更简单的原型语法
- 前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype
- 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,将 Person.prototype 重置到一个新的对象
- 注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函数
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 直接使用一个对象字面量对 原型对象进行赋值
Person.prototype = {
constructor : Person, // 需要手动 将 constructor 属性指向正确的构造函数
type : "human",
sayName : function () {
console.log(this.name);
}
};
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
person1.sayName();
console.log(person2.constructor);
原型对象使用建议
- 在定义构造函数时,可以根据成员的功能不同,分别进行设置:
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototypr 记得修正 constructor 的指向
内置构造函数的原型对象
JS原生构造函数的原型对象
所有函数都有 prototype 属性对象
JS中的内置构造函数也有 prototype 原型对象属性:
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- …
console.dir(Object.prototype);
console.dir(Function.prototype);
console.dir(Array.prototype);
console.dir(String.prototype);
console.dir(Number.prototype);
扩展数组的原型方法
- 不能直接给原型对象添加一个对象字母量的值
Array.prototype = {
// 增加一个获取偶数项的和
getEvenSum: function () {
// 获取数组中每一项的方式
// this[i]
var sum = 0;
for (var i = 0 ; i < this.length ; i++) {
if (i % 2 === 0) {
sum += this[i];
}
}
return sum;
}
};
// 定义一个数组
var arr = [2,4,5,7,9];
console.log(arr.getEvenSum());
console.dir(Array.prototype);
- 直接给原型对象添加一条新的属性(不允许更改内置的原型对象)
Array.prototype.getEvenSum = function () {
// 获取数组中每一项的方式
// this[i]
var sum = 0;
for (var i = 0 ; i < this.length ; i++) {
if (i % 2 === 0) {
sum += this[i];
}
}
return sum;
};
// 定义一个数组
var arr = [2,4,5,7,9];
console.log(arr.getEvenSum());
console.dir(Array.prototype);