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指向就是windowlet 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); // falseconsole.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的prototypeFn.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(); // rootconsole.log(f1 == f2); // falseconsole.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的属性和方法,都能被将来的实例使用
- 所有对象都具有的默认属性,自身是对象类型