JavaScript 中的面向对象概述
文章目录
对象和面向对象的概念
- 什么是对象:
- Everything is object (万物皆对象)。
- 从两次层次来理解: (1) 对象是单个事物的抽象。 (2) 对象是一个容器装了属性(property)和方法(method)
- 对象是单个事物的抽象
- JS 中的对象
- 在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集
- ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数
- 面向对象编程 (OOP)
- Object Oriented Programming,简称 OOP ,是一种编程开发思想
- 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟
面向对象与面向过程对比
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性
- 封装性
- 继承性
- [多态性] 抽象
面向对象的设计思想
- 抽象出 Class(构造函数)
- 根据 Class(构造函数) 创建 Instance(实例)
- 指挥 Instance 得结果
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、 处理数据、发出信息等任务。
- 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由 一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作 的大型软件项目。
JS 中创建对象的几种方式
- new Object() 构造函数
- 对象字面量 {}
- 工厂函数
- 自定义构造函数
new Object()
var person = new Object();
person.name = "Bob";
person.age = 18;
person.sayName = function () {
console.log(this.name);
};
对象字面量化简
var person1 = {
name : "Bob",
age : 18,
sayName : function () {
console.log(this.name);
}
};
var person2 = {
name : "Mike",
age : 20,
sayName : function () {
console.log(this.name);
}
};
工厂函数
function createPerson(name,age) {
// 添加一个新对象
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function () {
console.log(this.name);
};
// 必须有返回值
return person;
}
// 生成真正的对象
var person1 = createPerson("John",19);
自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person("Bob", 18);
var person2 = new Person("Mike", 20);
构造函数
new 关键字的用途
- 创建一个新对象
- 将函数内部的 this 指向了这个新对象
- 执行构造函数内部的代码
- 将新对象作为返回值
构造函数和实例对象的关系
- 构造函数是根据具体的事物抽象出来的抽象模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都通过一个 constructor 属性,指向创建该实例的构造函数
- 注意:constructor 是实例的属性的说法不严谨
- 后面可以修改 原型对象的 prototype 属性覆盖自己的 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); // Bob
// 使用构造函数调用实例成员会出错
// console.log(Person.name); // Person
/*
Person.sayName();
Uncaught TypeError: Person.sayName is not a function
*/
// 调用静态成员,只能通过构造函数进行调用
console.log(Person.version); // 1.0
console.log(person1.version); // undefined
构造函数的问题
造成内存浪费,同一个方法,指向不同引用
function Person(name,age) {
this.name = name;
this.age = age;
// this 内部的 type 属性值是不变的
this.type = "human";
// 每个对象的 sayName 方法也是一样的
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
console.log(person1.sayName === person2.sayName); // false
// 解决方法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 = "human";
this.sayName = sayName;
this.sayAge = sayAge;
}
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
console.log(person1.sayName === person2.sayName); // true
// 解决方法第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); // true
console.log(person1.sayAge === person2.sayAge); // true
更好的解决方法 prototype
使用原型对象可以更好的解决构造函数的内存浪费问题
prototype 原型对象
- 任何函数都具有一个 prototype 属性,该属性是一个对象。
- 可以在原型对象上添加属性和方法。
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在 函数。
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__
- 实例对象可以直接访问原型对象成员
// 定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 获取 构造函数 的 prototype 属性
console.log(Person.prototype);
// 属性值是一个对象,通常叫做原型对象
// 对象内部可以添加一些属性和方法
Person.prototype.type = "human";
Person.prototype.sayHi = function () {
console.log("hello");
};
// Person.prototype.constructor = Array;
// 构造函数的 原型对象上面都默认有一个 constructor 属性
// console.log(Person.prototype.constructor);
// 创建实例对象
var p1 = new Person("Mike",18);
// 所有的对象都有一个 __proto__ 的属性,是一个指针,指向的就是生成实例对象的 构造函数的原型对象
console.log(p1.__proto__);
console.log(p1.__proto__ === Person.prototype); // true
// __proto__ 属性并不是一个标准的属性,是浏览器自己根据语法自动生成的
// p1.__proto__.sayHi();
// 在真正开发的过程中,是不会书写 __proto__ 属性的
p1.sayHi();
// console.log(p1.constructor);
// console.log(p1 instanceof Person);
构造函数、实例、原型对象三者之间的关系
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向构造函数的原型对象
- 这个原型对象的所有属性和方法,都会被构造函数的实例对象所拥有
- 因此,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上
- 解决内存浪费问题
原型链
思考:为什么实例对象可以调用构造函数的 prototype 原型对象 的属性和方法?
原型链查找机制
- 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性:
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
实例对象读写原型对象成员
读取:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回 undefined
值类型成员写入(实例对象.值类型成员 = xx):
- 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉对原型对象成员的访问 引用类型成员写入(实例对象.引用类型成员 = xx):
- 同上
复杂类型成员修改(实例对象.成员.xx = xx):
- 同样会先在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 将所有实例共享的属性和方法,都添加给原型对象
Person.prototype.type = "human";
Person.prototype.sayName = function () {
console.log(this.name);
};
// 添加一个新的属性给原型对象,值是一个对象类型
Person.prototype.address = {
city : "北京"
};
// 生成对象实例
var person1 = new Person("Bob",18);
var person2 = new Person("Mike",20);
// 读取 属性和方法
// console.log(person1.type);
// console.log(person1.city);
// console.log(person1.sayName);
// person1.sayName();
// 通过实例对象添加新成员,会直接添加给自己,会屏蔽掉对原型对象的访问
person1.sex = "male";
person1.sayAge = function () {
console.log(this.age);
};
// 如果通过实例对象更改原型对象的属性和方法,会直接添加给自己,会屏蔽掉对原型对象的访问
person1.type = "person";
person1.sayName = function () {
console.log(this.name);
}
console.dir(person1);
// 通过实例对象更改原型对象中复杂类型数据中的内容,还是会进行原型链的查找
person1.address.city = "上海";
更简单的原型语法
- 前面在原型对象每添加一个属性和方法就要书写一遍 Person.prototype
- 为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原 型对象,将 Person.prototype 重置到一个新的对象
- 注意:原型对象会丢失 constructor 成员,所以需要手动将 constructor 指向正确的构造函 数
// 自定义构造函数
function Person(name,age) {
this.name = name;
this.age = age;
}
// 将所有实例共享的属性和方法,都添加给原型对象
// Person.prototype.type = "human";
// Person.prototype.sayName = function () {
// console.log(this.name);
// };
// 直接使用一个对象字面量对 原型对象进行赋值
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);
原型对象使用建议
- 在定义构造函数时,可以根据成员的功能不同,分别进行设置:
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
对象之间的继承
对象拷贝 for……in :父对象的属性拷贝给子对象
// 父级的对象
var laoli = {
name: "laoli",
money: 1000000,
house: ["商铺", "住宅"],
tech: function () {
console.log("厨艺")
}
};
// 子级的对象
var xiaoli = { name: "xiaoli" }
// 封装一个对象之间继承的函数
function extend(parent, child) {
for (var k in parent) {
// 子级有的属性不需要继承
if (child[k]) continue;
child[k] = parent[k];
}
}
// 调用函数实现继承
extend(laoli,xiaoli);
console.log(xiaoli);
原型继承
原型继承 : 将子类的 prototype 属性 赋值给 父类的实例对象上
// 封装的构造函数就是用来创建一类对象
// 继承指的是 类型 和 类型之间的继承
// 学生类型 老师类型 --> 抽象,提取所有的公共的属性,放到一个 父类型中
// 当前学习阶段,没有一个专门的用来继承的方法
// 人类类型
function Person(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 学生类型
function Student(score) {
this.score = score;
}
// 老师类型
function Teacher(salary) {
this.salary = salary;
}
// 原型对象,可以将自己的属性和方法继承给将来的实例对象使用
Student.prototype = new Person("zs",18,"男");
Student.prototype.constructor = Student;
// 生成一个实例
var s1 = new Student(89);
var s2 = new Student(100);
console.dir(s1);
console.dir(s2);
console.log(s1.name);
console.log(s1.constructor);
构造函数的属性的继承
// 人类类型
function Person(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 学生类型
function Student(name,age,sex,score) {
// 直接对父类型的构造函数进行一个普通调用
// Person 普通调用过程中,内部的 this 指向的是 window
// 可以通过 call 方法更改Person 内部的 this
Person.call(this,name,age,sex);
this.score = score;
}
// 老师类型
function Teacher(name,age,sex,salary) {
Person.call(this,name,age,sex);
this.salary = salary;
}
// 创建学生的实例对象
var s1 = new Student("zs",18,"男",89);
var s2 = new Student("ls",19,"男",92);
console.dir(s1);
console.dir(s2);
构造函数(父类)的方法的继承
//
// 人类类型
function Person(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 父类型的原型对象中有方法也需要继承
Person.prototype.sayHi = function () {
console.log("你好");
};
// 学生类型
function Student(name,age,sex,score) {
Person.call(this,name,age,sex);
this.score = score;
}
// 子类型的原型对象上,需要继承父类型原型对象的方法
// 方法1:对象拷贝继承
// for (var k in Person.prototype) {
// // 保留自己的 constructor 不要进行继承
// if (k === "constructor") {
// continue;
// }
// Student.prototype[k] = Person.prototype[k];
// }
// 方法2:原型继承
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 老师类型
function Teacher(name,age,sex,salary) {
Person.call(this,name,age,sex);
this.salary = salary;
}
// 创建学生的实例对象
var s1 = new Student("zs",18,"男",89);
var s2 = new Student("ls",19,"男",92);
console.dir(s1);
console.dir(s2);
s1.sayHi();
借助构造函数继承属性
// 组合继承:属性在构造函数内部继承,方法通过原型继承
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("你好");
}
// 生成一个子类型
function Teacher(name,age,salary) {
// 继承父类的属性
Person.call(this,name,age);
this.salary = salary;
}
// 方法继承,通过原型对象继承
Teacher.prototype = new Person();
Teacher.prototype.constructor = Teacher;
// 生成老师的一个实例
var t1 = new Teacher("wang",45,10000);
console.dir(t1);
console.log(t1.name);
t1.sayHi();