1、引言
初学者在学习面对对象时可能总是会被绕晕,也理解不了面对对象中各种名词,但是了解这些概念在面对对象编程中极为重要,也意味着你能否学懂面对对象编程。下面我会利用已知探索未知去讲解面对对象,帮助大家理解。
2、创建对象
- 不知道大家是否还记得,在创建数组时,有两种创建方式。一种是字面量创建,即
let arr = [2, 3, 4];
,另一种是构造函数创建,通过关键字new
来创建,即let arr = new Array(2, 3, 4);
。 - 这里的
Array
是一种内置构造函数。通过new
执行构造函数,创建数组(数组也是对象)。 new
执行方式:执行函数,凡是用new执行的函数,都叫构造函数。- 也就是说我们可以自己创建自定义构造函数。这里我们先说一下
new
的原理:
- new的原理:
1、创建了一个新对象
2、修改了函数中的this指向,指向第1步创建的新对象
3、检测原函数中是否主动返回对象,如果没有,那么返回第1~3步创建的对象
4、并将新对象中的__proto__指向了该构造函数的prototype
先不用理解,继续向下看
- 创建自定义构造函数:
function Fn() { // 行业规范,构造函数的函数名采用大驼峰式
console.log(this);
}
Fn(); // 普通执行:this指向就是window
let f1 = new Fn(); // new执行:this的指向是new执行后构造函数的同名对象(new的原理第2点)
console.log(f1); // f1是new执行的返回值,函数同名对象 Fn{}(new的原理第1、4点)
let f2 = new Fn();
console.log(f1 == f2); // false 每次new执行创建的都是新对象,两两不相等
- 1、每次
new
的执行都会创建一个新对象,即上述代码中的f1、f2
,且这些对象两两不相等。 - 2、通过
new
的执行修改了函数中this
的指向,指向执行时创建的新对象,即指向上述代码中的f1、f2
- 3、如果构造函数不主动返回对象,那它的返回值就是每次执行时创建的新对象
- 4、(不急,这点需要结合原型)
循序渐进,来看看下面这段代码,看懂后我们再进行原型的介绍。
function Fn(n) {
// this指向对象,给new出来的对象添加属性
this.name = n; // admin
}
let f1 = new Fn("admin"); // new执行时传参
console.log(f1); // Fn{name:"admin)
3、原型
- 创建一个自定义构造函数Fn
function Fn(n) {
this.name = n;
this.show = function() {
console.log(this.name);
}
}
// 通过new创建一个对象
let f1 = new Fn("admin");
console.log(f1); // Fn {name: "admin", show: ƒ}
f1.show(); // admin
// 通过new再创建一个对象
let f2 = new Fn("root");
console.log(f2); // Fn {name: "root", show: ƒ}
f2.show(); // root
// 这两个对象不相等,且具有相同功能的show函数也不等
console.log(f1 == f2); // false
console.log(f1.show == f2.show); // false 两个对象的show方法不相等,与下面做对比
- 与数组的构造函数式创建来对比
let arr1 = new Array(2, 3, 4);
let arr2 = new Array(4, 5, 6);
console.log(arr1 == arr2); // false
// push是数组的方法之一
console.log(arr1.push == arr2.push); // true 数组的方法是相等的,与上面做对比
- 也就是说,到这一步我们的自定义构造函数并未创建成功,我们的需求是让
console.log(f1.show == f2.show)
的结果为true
。 - 先卖个关子,给一个已知条件:内置构造函数创建的对象的方法,都被绑定在了自己的构造函数的原型对象
(prototype)
身上。 - 比如数组的
push
方法,就被绑定在了数组的构造函数的原型对象身上Array.prototype.push
- 先来看看这个
prototype
是个啥
console.log(Array.prototype); // 一个伪数组,包含了数组所有的方法
- 继续使用数组来举例,我瞎编一个
sayHello
的方法,在arr1
身上肯定没有sayHello
这个方法,如果想通过arr1
来执行sayHello
方法
// 接上面代码
// arr1.sayHello(); // 如果直接通过arr1来执行这个方法,报错,必须先绑定在arr1身上
arr1.sayHello = function() {
console.log("hello")
}
arr1.sayHello(); // hello
- 如果我想让
arr2
也执行sayHello
方法,也必须要再绑定再arr2
身上,如果我想让arr3、arr4..
等执行这个方法,就得一个一个绑定,很耗性能。 - 但是,如果将这个方法绑定在数组的构造函数的原型身上,如下
Array.prototype.sayHello = function() {
console.log("hello");
}
// 比如我想让arr2来执行sayhello(在上面的案例中,arr2未绑定此方法)
arr2.sayHello(); // hello
- 执行成功了,就算创建无数个数组,它们都能执行
sayHello
这个方法,这个叫做"继承"。
- prototype的原理:
在对象自身拥有一个内置属性:proto,每当在构造函数的原型对象上如(Array.prototype)绑定一个方法时,都相当于在每个实例身上的__proto__这个属性里绑定了这个方法。
* 这就是new原理的第4点。
-
这个
__proto__
可以在浏览器上看到(注意proto
左右是两个下划线),原型身上的属性或方法可以在这里找到 -
由于一个实例至少会有两个
__proto__
(一个是自身的构造函数的,另一个是顶层原型对象的),所以原型也称为原型链
-
现在回到需求,如果把
show
方法绑定给自定义构造函数Fn
的原型对象
function Fn(n) {
this.name = n;
// 不在构造函数内部设置show方法
// this.show = function() {
// console.log(this.name);
// }
}
// 把show方法绑定给Fn的prototype
Fn.prototype.show = function() {
console.log(this.name);
}
// 通过new创建一个对象
var f1 = new Fn("admin");
console.log(f1); // Fn {name: "admin", show: ƒ}
f1.show(); // admin,就算f1自身没有绑定show方法,仍能执行
// 通过new再创建一个对象
var f2 = new Fn("root");
console.log(f2); // Fn {name: "root", show: ƒ}
f2.show(); // root
console.log(f1 == f2); // false
console.log(f1.show == f2.show); // true,相等了
- 相等了,也就是说我们的自定义构造函数创建成功了
至此,面对对象的基础使用就已经结束,下面总结面对对象的语法、代码及概念
4、总结
- 语法
- 面对对象的语法:
1、属性写在构造函数内部的this身上
2、方法写在构造函数的原型(prototype)上
3、原型身上的方法在被实例执行时,其内部的this依然指向实例
- 代码
function Fn(n) {
this.name = n;
}
Fn.prototype.show = function() {
console.log(this.name);
}
let f = new Fn("参数");
f.show();
- 概念
- 1、实例:上述所有通过
new
执行创建的对象,都称为实例 - 2、原型:
- 构造函数的prototype(显式原型)
- 是当前构造函数身上的一个属性,自身是对象类型
- 专门作为将来的实例的__proto__的指向
- 所有添加给prototype的属性和方法,都能被将来的实例使用
- 实例的__proto__(隐式原型)
- 所有对象都具有的默认属性,自身是对象类型
- 指向了:构造自身的构造函数的原型prototype
- 当实例身上没有某个方法或属性时,默认查找__proto__的方法或属性
- 构造函数的prototype(显式原型)
- 1、实例:上述所有通过