JavaScript 面向对象之构造函数模式
构造函数模式的目的
构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。
构造函数可以明确出类和实例的关系。
function Person(name, age) {
// 浏览器默认创建的对象就是我们的实例 p1 -> this
// 变量 p1 实例化的时候,那么 this 就是 p1,相当于 p1.name=name
this.name = name;
this.age = age;
this.writeJs = function () {
console.log("my name is " + this.name + ",i can write js 啦!!");
};
// 浏览器在把创建的实例默认的进行返回,无需 return
}
//Person->this p1 用的时候需要 new 一下;创造一个构造函数的实例;
var p1 = new Person("zhu", 18);
var p2 = new Person("an", 28);
p1.writeJs(); //writeJs->this p1
console.log(p1); // 顺着原型,可以看到他的__proto__指向 Person 这个构造函数;
构造函数的注意事项
用的时候需要 new 一下;一定要切记!!!
创造一个构造函数的实例,如果不写 new,上面的函数没有任何意义
错误的用法
var res = Person("zhu", 17);
// 这样写不是构造函数模式执行而是普通的函数执行
// 由于没有写 return 所以 res=undefined
// 并且 Person 这个方法中的 this 是 window
console.log(res);
构造函数模式和工厂模式的区别
- 1、没有显式地创建对象;
- 2、直接将属性和方法赋给了 this 对象;
- 3、没有 return 语句。
- 4、执行的时候,需要写 new
- 普通函数执行 ->
Person()
- 构造函数模式 ->
new Person()
通过 new 执行后,我们的 Person 就是一个类了,而函数执行的返回值 (p1) 就是 Person 这个类的一个实例
- 普通函数执行 ->
- 5、在函数代码执行的时候
- 相同:都是形成一个私有的作用域
- 然后 形参赋值 ->预解释 ->代码从上到下执行 (类和普通函数一样,它也有普通的一面)
- 不同:在代码执行之前,不用自己在手动的创建 obj 对象了
- 无显示创建 obj 的过程,浏览器会默认的创建一个对象数据类型的值,这个对象其实就是我们当前类的一个实例
- 接下来代码从上到下执行,以当前的实例为执行的主体 (this 代表的就是当前的实例), 然后分别的把属性名和属性值赋值给当前的实例
- 最后浏览器会默认的把创建的实例返回
- 相同:都是形成一个私有的作用域
- 6、构造函数模式的函数,推荐首字母大写;
- 按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
- 这个做法借鉴自其他 OOP 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
New 的过程发生了什么
要创建 Person 的新实例,必须使用 new 操作符
会经历四个步骤;
- (1) 创建一个新对象;
- (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- (3) 执行构造函数中的代码(为这个新对象添加属性);
- (4) 返回新对象。
在前面例子的最后, p1 和 p2 分别保存着 Person 的一个不同的实例。
这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示
var p1 = new Person("zhu", 18);
var p2 = new Person("an", 28);
console.log(p1.constructor == Person); //true
console.log(p1.constructor == Person); //true
原理图如下
下面三种的对比都是 true;
console.log(p1.constructor == Person); //true
console.log(p1.constructor == Person); //true
console.log(p1.__proto__.constructor == Person); //true
console.log(p1.__proto__.constructor == Person); //true
console.log(p1.__proto__ == Person.prototype); //true
console.log(p1.__proto__ == Person.prototype); //true
构造函数模式的优点:解决了工厂模式中的归属问题;
对象的 constructor
属性最初是用来标识对象类型的。
但是,提到检测对象类型,还是 instanceof
操作符要更可靠一些。
我们在这个例子中创建的所有对象既是 Object 的实例,同时也是 CreateJsPerson,这一点通过 instanceof 操作符可以得到验证。
console.log(p1 instanceof Person); //true
console.log(p1.instanceof Object); //true
console.log(p2 instanceof Person); //true
console.log(p2.instanceof Object); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
在这个例子中, p1
和 p2
之所以同时是 Object
的实例,是因为所有对象均继承自 Object
。
构造函数与其他函数的区别
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。
- 任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;
- 而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
function Person(name, age) {
this.name = name;
this.age = age;
this.writeJs = function () {
console.log("my name is " + this.name + ",i can write js 啦!!");
};
}
// 当作构造函数使用
var p1 = new Person("zhu", 18);
var p2 = new Person("an", 28);
p1.writeJs();
// 作为普通函数调用
Person("hehe", 27); // 属性和方法都被添加给 window 对象了
window.writeJs();
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "haha", 25);
o.writeJs();
当在全局作用域中调用一个函数时, this 对象总是指向 Global 对象(在浏览器中就是 window 对象)。
因此,在调用完函数之后,可以通过 window 对象来调用 writeJs() 方法,并且还返回了值。
最后,也可以使用 call()
(或者 apply()
)在某个特殊对象的作用域中调用 Person() 函数。
这里是在对象 o 的作用域中调用的,因此调用后 o 就拥有了所有属性和 writeJs()
方法。
构造函数模式的缺点;
p1 和 p2 拥有不同的属性值,但是拥有相同的方法;
var p1 = new Person("zhu", 18);
var p2 = new Person("an", 28);
console.log(p1.writeJs === p2.writeJs);
函数的意义是高内聚,低耦合,避免同样的事情重复创建,所以才把做相同事务的代码封在一起;
构造函数中的函数已经失去了函数的意义了;
Person 中的 writeJs 方法,在实例化以后,是不同的;p1.writeJs 并不等于 p2.writeJs;
当然这个问题,也可以通过变种来实现;
function Person(name, age) {
this.name = name;
this.age = age;
this.writeJs = writeJs
}
function writeJs(){
console.log("my name is " + this.name + ",i can write js 啦!!");
}
// 当作构造函数使用
var p1 = new Person("zhu", 18);
var p2 = new Person("an", 28);
console.log(p1.writeJs === p2.writeJs);//true
因为 writeJs 是定义在全局作用域中的,所以 p1.writeJs 和 p2.writeJs 的引用地址都是全局中的 writeJs,引用地址相等,所以为 true
;
但是这样做还有一个非常问题,就是 Person 这个类,依赖 writeJs 这个方法,两者耦合在一起了,维护起来非常不方便;
如果对象需要定义很多方法,使用自定义的全局函数,我们这个自定义类就丝毫没有封装性可言了。
构造函数的加深理解;
function Fn() {
this.x = 100;
this.getX = function () {
//this->需要看 getX 执行的时候才知道
console.log(this.x);
}
}
var f1 = new Fn ;// 当 f1 作为 Fn 的一个实例的时候,里面的 this 就是 f1
f1.getX(); //->方法中的 this 是 f1 ->100
var ss = f1.getX;
ss(); //->方法中的 this 是 window ->undefined
-
1、在构造函数模式中 new Fn() 执行,如果 Fn 不需要传递参数的话,后面的小括号可以省略,JS 中所有的类都是函数数据类型的,它通过 new 执行变成了一个类,但是它本身也是一个普通的函数,JS 中所有的实例都是对象数据类型的;
-
2、this 的问题:在类中出现的 this.xxx=xxx 中的 this 都是当前类的实例,而某一个属性值(方法), 方法中的 this 需要看方法执行的时候,前面是否有".“才能知道 this 是谁;如果函数只是定义,没有引用,那么里面的 this 是不知道是谁的,函数只有被引用了,才知道引用被执行时的 this 指向谁;
-
3、类有普通函数的一面,当函数执行的时候,var num 其实只是当前形成的私有作用域中的私有变量而已,它和我们的 f1 这个实例没有任何的关系;只有 this.xxx=xxx 才相当于给 f1 这个实例增加私有的属相和方法,才和我们的 f1 有关系。..
function Fn() { var num = 10; this.x = 100;//f1.x=100 this.getX = function () {//f1.getX=function... console.log(this.x); } } var f1 = new Fn; console.log(f1.num);//->undefined
-
4、在构造函数模式中,浏览器会默认的把我们的实例返回(返回的是一个对象数据类型的值);
- 如果我们自己手动写了 return 返回:返回的是一个基本数据类型的值,当前实例是不变的,例如:
return 100;
我们的 f1 还是当前 Fn 类的实例; - 但是返回的如果是一个引用数据类型的值,当前的实例会被自己返回的值给替换掉,例如:
return {name:"zhu"}
我们的 f1 就不在是 Fn 的实例了,而是对象{name:“zhu”}
function Fn() { this.x = 100; this.getX = function () { console.log(this.x); }; return {name: "zhu"}; } var f1 = new Fn; console.log(f1);// 此时的 f1 是{name: "zhu"}
- 如果我们自己手动写了 return 返回:返回的是一个基本数据类型的值,当前实例是不变的,例如:
-
5、检测某一个实例是否属于这个类 ->
instanceof
- 对于检测数据类型来说,typeof 有自己的局限性,不能细分 object 下的对象、数组、正则。
function Fn() { this.x = 100; this.getX = function () { console.log(this.x); }; } var f1 = new Fn; console.log(f1 instanceof Array);//->false 说明 f1 不是一个数组 console.log(f1 instanceof Fn);//->true 说明 f1 是 Fn 的一个实例 console.log(f1 instanceof Object);//->true 说明 a 是一个对象
-
6、下面 f1 和 f2 都是 Fn 这个类的一个实例,都拥有 x 和 getX 两个属性,但是这两个属性是各自的私有的属性;
function Fn() { this.x = 100; this.getX = function () { console.log(this.x); }; } var f1 = new Fn; var f2 = new Fn; console.log(f1.getX === f2.getX);//->false