本文是博主自己归纳曾学到的知识,以及分享工厂模式、构造函数、以及原型链之间的用途;有很多面试者都会遇到这些问题,但是一些小白来说,还是不清楚,这些知识点到底在技术中有什么用途,到底是干什么的?今天我就简单说一说,本篇博客会从基本开始介绍,如果你对今天的内容所致甚少,并且你有耐心看完,相信你会有超值的收获!!!
小白们都对原型链迷迷糊糊,经常查阅相关文章,却还是缕不清到底是干什么的。因为你不知道它到底有什么用,又是如何会用到它,所以在直接查阅原型链的时候,忽略了它之前很重要的知识,所以你不懂。这个很重要的知识,就是构造函数!!!
本篇博客要一环扣一环,决定帮大家捋清思路,所以我们从基本开始说,从头到尾请跟住博主的思路。
面向对象:
对象?无论在javascipt当中,还是java当中,或其他语言中,所提及的对象,绝对不是男女朋友的对象,所以我们凡人先忘掉感情视野,再体会一下这个词:“对象”。
所谓对象,任何事物都可以称作是对象。
比如:
医生给病人治疗,病人对于医生来说,就是治疗的对象。
我们去商场购买苹果,苹果对于我们来说,就是要购买的对象。
博主写的博客,你们就是我要展示博客的对象。
科学家要研制火箭,火箭对于科学家来说,就是要研制的对象
……
所以我们这些都是对象,有句英文说的好,Everything is object(万物皆对象)。
知道对象了,那为什么要“面向”呢?“面向对象又是什么?”
这就是一种编程思想,面向对象编程。在我们的开发世界中,把所有的方法都封装成一个一个的对象,大的功能都是由一个又一个的对象实现的,所以就叫面向对象编程思想。在程序员眼中,一切功能都是被拆分成小的单独对象,组装起来的。
在开发中,我们很容易可以创造出一个对象,例如:
var person = {}; // 字面量创建一个对象
var person1 = new Object(); // 构造一个对象
两种方法,但都是创造了一个对象。对象都有自己的属性,比如人可以吃饭,睡觉,恋爱。这些都是我们人(对象)的属性,同样,我们也可以在上面的例子中,加入属性:
var person = {
name: 'villin',
age: 18,
exes: ["111", "222", "333"]
};
我们也可以删除里边的属性,比如把年龄删除:
delete person.age
这些就是对象的含义,我们在开发中,也可以创建一个函数,这个函数就看作是一个对象,里边些不同的方法。面向对象很简单,反复的看几遍,就能把这个“对象”一词理解明白。
this:
this,是属于一个指向的关键字,一般是在函数内使用,谁调用了这个函数,this指向谁。这个不太好解释,只能体会的去理解。看下边这个例子,在person中定义sayHi这个函数,所以this指向了person对象中的属性。
var person = {}
person.name = 'villin';
person.sayHi = function () {
console.log(this.name)
}
person.sayHi(); // villin
this重点:
1、在函数内,函数定义时无法确定this指向,所以记住:谁调用,指向谁!
2、函数如果以普通方式进行调用,this指向window
3、每个函数都有自己的this,只要进入一个函数,this就可能发生变化
那么如果在函数中还不想让this指向window,还想使用传过来的参数,我们习惯于将参数赋值:
function aaa(name){
this.name = name
}
工厂方法(factory):
我们把上边刚才的例子,放在一个函数中,并返回这个值,就是工厂模式。具体为啥叫这个名字,我也不知道。我们封装一个简单的(工厂模式)函数,并且进行调用。这个函数很简单,我标注的很详细,仔细看,这一环很重要。
// 创建一个函数,接收两个参数,一个name,一个age (函数可以接收很多参数,具体几个看你们自己的心情)
function person(name,age){
// 1.创建一个对象
var a = {};
// 2.在对象上添加属性和方法,将参数赋值给它
a.name = name;
a.age = age;
// 3.和上边一样,这个对象a中定义一个函数sayHi
a.sayHi = function(){
console.log("你好,我叫"+this.name+",我今年"+this.age+"岁")
}
// 4.创建的对象必须要返回这个对象a,才能打印出函数中的内容;
return a;
}
好了,这个工厂模式的函数,就封装好了,就可以多次调用这个封装的方法了,例如:
// 定义三个p,分别传入两个参数,一个name,一个age
var p1 = person("villin",25)
var p2 = person("xiaoming",26)
var p3 = person("xiaoli",18)
// 分别调用三个方法
p1.sayHi()
p2.sayHi()
p3.sayHi()
打印结果如下:
我们有个方法instanceof
,是可以检测一个对象是否为一个构造函数的实例,刚才写的我们可以检测一下:
console.log(p1 instanceof person) // false
返回为false,说明p1并不是person函数的实例,那么如果我在创建一个其他的函数,也是检测不出来的,这样在项目中,如果很多个函数,我们就无法得知,这个对象是哪个函数的实例,所以工厂模式的缺点,就是无法判断生成的对象,是哪个构造函数的实例。
解决办法:使用构造函数的方法来创建对象!!!
延伸:实例和继承。
什么是实例?举个例子:我们刚才说过,万物皆对象,那么动物就是个对象,我们再具体一点,猫、狗、老鼠这些就是动物的实例。所以person函数就是一个对象,那么p1、p2、p3是否是person的实例,就用instanceof来验证;
那么猫,狗等都继承了动物的特征,比如动物是活的,有眼睛耳朵鼻子等,猫狗也有这些,这就是继承,继承了动物的特征。但是不能反过来说动物也继承了猫狗的特征,这是不对的,因为猫有毛,但是鱼没有。
万物皆对象,所以实例(像猫和狗)也是对象,但是对象未必是实例(例如动物是个大的框架,没有被细分成实例)。
构造函数(constructor):
我们接着上边的案例来说,同样我们创建一个函数person,但是这个函数名首字母要大写,代表构造函数,但并非规定。
// 函数名大写
function Person(name,age){
// 省略了创建对象 (和工厂模式对比)
// 省略了添加属性和方法 (和工厂模式对比)
// 1.赋值指针this
this.name = name;
this.age = age;
this.sayHi = function () {
console.log("你好,我叫"+this.name+",我今年"+this.age+"岁")
}
// return返回对象,也省了 (和工厂模式对比)
}
接下来我们重新构造(new)这个Person对象,并传入相应的参数:
p1 = new Person("villin",25) // 不要忘记大写
p2 = new Person("xiaoming",26)
p3 = new Person("xiaoli",18)
// 分别调用三个方法
p1.sayHi()
p2.sayHi()
p3.sayHi()
打印结果,同样如此:
我们再次尝试用instanceof
验证一下
console.log(p1 instanceof Person) // true
显然,可以被验证了,这也说明了,构造函数p1为Person的实例。如果项目中不光有Person函数,或其他,也都可以验证。(自己要回头去看一下工厂模式和构造函数的区别)
不过构造函数依然伴有缺点:
细心发现,我们刚才构造了三个函数p1~p3,分别构造了三次,这样每次构造一次,就会生成一个sayHi的函数(好奇者可以分别打印p1、p2、p3在控制台看一下,会发现生成了三次相同的函数),并且这些函数对象内的方法,其代码是一模一样的,都是(你好,我的名字是,年龄)等,这样重复太多,复用性不高,太浪费资源。
想解决这个问题,我们就需要把这个sayHi公用的函数存放到一个位置,这个位置要确保每个对象都能访问到。
那么这个位置就是:prototype(原型)。
原型(prototype):
1、prototype中文含义就是“原型”,只要是函数,都有自己的原型prototype。
2、当用构造函数创建对象(new Xxx())时,浏览器在新创建的对象上添加了一个属性__proto__(前后是双下划线。不要直接使用)。
所以要记住,对象中有一个属性是__proto__。函数也是对象,所以它也有__proto__,但是函数还多了一个prototype属性。
这个对象到底长什么样,我们打印一下这个对象p1(刚刚的p1)看一下。
function Person(name,age){
// 省略了创建对象 (和工厂模式对比)
// 省略了添加属性和方法 (和工厂模式对比)
// 1.赋值指针this
this.name = name;
this.age = age;
this.sayHi = function () {
console.log("你好,我叫"+this.name+",我今年"+this.age+"岁")
}
// return返回对象,也省了 (和工厂模式对比)
}
p1 = new Person("villin",25)
console.log(p1) // 打印p1
可以看到,我们没有打印函数,仅仅是打印出的p1,那为什么也会有name、age、以及sayHi属性?这就是继承,因为new构造出来的是Person函数的实例,所以它继承了它构造的函数Person中的属性。
我们可以看到这个对象p1是带有自己的一个属性__proto__的,并且,这个对象的__proto__指向了它的构造函数的prototype(每个函数都是有prototype的属性的,只是没打印而已,自己印在自己的脑海里),并且__proto__也会继承prototype中的属性。
这些就是概念性的东西,要试着去理解,如果不理解多看几遍,一定要慢慢的读,博主写这些,也是慢慢的写的。
那么既然每个构造函数都会继承它原来函数中prototype的属性,那么就利用这一点,写一个可以公用的方法。
让他们的实例公用这个sayHi函数,我们开始利用这个原型prototype去更改构造函数的不足之处:
function Person() {} //单独设一个空的函数,它有一个属性是prototype
// 公共的部分
// 我们可以直接更改Person的原型prototype
Person.prototype.name = 'villin';
Person.prototype.age = 26;
Person.prototype.sayHi = function () {
console.log("你好,我叫"+this.name+",我今年"+this.age+"岁")
}
var p1 = new Person();
var p2 = new Person();
p1.sayHi();
p2.sayHi();
// 你好,我叫villin,我今年26岁
// 你好,我叫villin,我今年26岁
可以看到,虽然还是创建了两个函数,但是p1和p2都复用了prototype中的属性,所以打印的东西都是一样的。
原型链:
疑问:同学可能会发现,我并没有在p1和p2中定义任何的属性,直接调用p1.sayHi为什么可以把原型prototype中的sayHi打印出来?
p1中并没有sayHi这个属性。这就引入了一个词叫:原型链。
当访问一个对象时,会在对象内进行查找这个属性,比如之前定义过的this.sayHi属性。像本次,对象内没有定义sayHi属性,那么就会进入p1的原型__proto__中查找,因为__proto__指向了Person中prototype,并继承了其中的属性,所以prototype中有啥,__proto__就有啥,所以就找到了。这一条链式查找就是原型链。我们打印一下p1看一下:
不过,这个方法还是有些缺点,因为p1和p2都继承了相同的属性,打印出来的结果都是一样的,这并不是我们想要的,我们想要的是,公用一些方法外,可以传递想要打印的参数,想打印出什么值,就打印出什么值。
所以,最后的解决方案:组合(构造+原型)
构造函数+原型:
如果以上的几个方法都看明白了,这个就很简单,只需要在原型中定义想要得到的公用方法,在函数中定义this指向,并接收参数即可:
// 需要定义两个函数,一个是构造函数Person,一个是在Person的prototype中定义公用方法sayHi函数。
function Person(name, age, exes) {
//将所有的参数属性,放在构造方式中
this.name = name;
this.age = age;
this.exes = exes;
}
Person.prototype.sayHi = function () {
// this会指向Person内
console.log("你好,我叫"+this.name+",我今年"+this.age+"岁")
}
然后照常传递参数,正常打印:
p1 = new Person("villin",25)
p2 = new Person("xiaoli",18)
// 你好,我叫villin,我今年25岁
// 你好,我叫xiaoli,我今年18岁
这样既有公用的继承函数方法,又可以传递参数,可以获得自己想要的数据。
我们最后可以在验证一下是否是Person的实例问题。
console.log(p1 instanceof person) // true
这就是今天所写的内容,在最后,补充一下知识,也能会成为面试的考点:
问题:
构造函数在new的时候,JS引擎做了那些事情?(new之后发生了什么?)
- 创建了一个对象
- 将构造函数内的this,指向这个对象
- 执行构造函数
- 返回这个对象
weChat:VillinWeChat
欢迎提出宝贵意见