构造函数、原型、原型链详解

简单数据类型和复杂数据类型

简单数据类型:包括类型number string Boolean undifined null

复杂数据类型:又称为引用类型,包括Object Array Date function

主要区别是在内存中的存储过程,存放在栈内存中还是存放在堆内存中

普通对象与函数对象

JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。下面举例说明

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

构造函数

1.什么是构造函数

在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。

2.为什么要使用构造函数

举个例子,我们要录入一年级一班中每一位同学的个人信息,那么我们可以创建一些对象,比如:

let p1 = { name: 'zs', age: 6, gender: '男', hobby: 'basketball' };
let p2 = { name: 'ls', age: 6, gender: '女', hobby: 'dancing' };
let p3 = { name: 'ww', age: 6, gender: '女', hobby: 'singing' };
let p4 = { name: 'zl', age: 6, gender: '男', hobby: 'football' };
// ...

像上面这样,我们可以把每一位同学的信息当做一个对象来处理。但是,我们会发现,我们重复地写了很多无意义的代码。比如 name、age、gender、hobby 。如果这个班上有60个学生,我们得重复写60遍。
这个时候,构造函数的优势就体现出来了。我们发现,虽然每位同学都有 name、gender、hobby 这些属性, 但它们都是不同的,那我们就把这些属性当做构造函数的参数传递进去。而由于都是一年级的学生,age 基本都是6岁,所以我们就可以写死,遇到特殊情况再单独做处理即可。此时,我们就可以创建以下的函数:

function Person(name, gender, hobby) {
    this.name = name;
    this.gender = gender;
    this.hobby = hobby;
    this.age = 6;
}

当创建上面的函数以后, 我们就可以通过 new 关键字调用,也就是通过构造函数来创建对象了

let p1 = new Person('zs', '男', 'basketball');
let p2 = new Person('ls', '女', 'dancing');
let p3 = new Person('ww', '女', 'singing');
let p4 = new Person('zl', '男', 'football');
// ...

此时你会发现,创建对象会变得非常方便。所以,虽然封装构造函数的过程会比较麻烦,但一旦封装成功,我们再创建对象就会变得非常轻松,这也是我们为什么要使用构造函数的原因。
在使用对象字面量创建一系列同一类型的对象时,这些对象可能具有一些相似的特征(属性)和行为(方法),此时会产生很多重复的代码,而使用构造函数就可以实现代码的复用。

3.构造函数的执行过程

先说一点基本概念

function Animal(color) {
  this.color = color;
}

当一个函数创建好以后,我们并不知道它是不是构造函数,即使像上面的例子一样,函数名为大写,我们也不能确定。只有当一个函数以 new 关键字来调用的时候,我们才能说它是一个构造函数。就像下面这样:

let dog = new Animal("black");

我们还是以上面的 Animal 为例。
(1) 当以 new 关键字调用时,会创建一个新的内存空间,标记为 Animal 的实例。
(2) 函数体内部的 this 指向该内存
(3) 执行函数体内的代码
(4) 默认返回 this 。

由于函数体内部的 this 指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间,此时该内存空间被变量dog所接受, 同时被标记为 Person 的实例。

4.构造函数的返回值

构造函数执行过程的最后一步是默认返回 this 。言外之意,构造函数的返回值还有其它情况。下面我们就来聊聊关于构造函数返回值的问题。
(1)没有手动添加返回值,默认返回 this 。
(2) 手动添加一个基本数据类型的返回值,最终还是返回 this。

function Person2() {
  this.age = 28;
  return 50;
}

var p2 = new Person2();
console.log(p2.age);   // 28
p2: {
  age: 28
}

如果上面是一个普通函数的调用,那么返回值就是 50。
(3) 手动添加一个复杂数据类型(对象)的返回值,最终返回该对象

function Person3() {
  this.height = '180';
  return ['a', 'b', 'c'];
}

var p3 = new Person3();
console.log(p3.height);  // undefined
console.log(p3.length);  // 3
console.log(p3[0]);      // 'a'
function Person4() {
  this.gender = '男';
  return { gender: '中性' };
}

var p4 = new Person4();
console.log(p4.gender);  // '中性'
5.构造函数属性

先复习一下构造函数的知识:

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function() { alert(this.name) } 
}
var person1 = new Person('Zaxlct', 28, 'Software Engineer');
var person2 = new Person('Mick', 23, 'Doctor');

上面的例子中 person1 和 person2 都是 Person 的实例。这两个实例都有一个 constructor (构造函数)属性,该属性(是一个指针)指向 Person。 即:

  console.log(person1.constructor == Person); //true
  console.log(person2.constructor == Person); //true

实例的构造函数属性(constructor)指向构造函数。

原型对象

1.为什么要使用原型对象

我们知道,在一个对象中可能不仅仅存在属性,还存在方法

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.say = function() {
        console.log('Hello');
    };
}

var p1 = new Person('Tom', 18);
p1.say();  // 'Hello'
var p2 = new Person('Jack', 34);
p2.say();  // 'Hello'

我们发现,实例 p1 和 实例 p2 调用了相同的方法,都打印出 Hello 的结果。但是,它们的内存地址是一样的么?我们打印看看:

console.log(p1.say == p2.say); // false

结果为 false 。因为每一次通过构造函数的形式来调用时,都会开辟一块新的内存空间,所以实例 p1 和 p2 所指向的内存地址是不同的。但此时又会有一个尴尬的问题,p1 和 p2 调用的say 方法,功能却是相同的,如果班里有 60 个学生,我们需要调用 60 次相同方法,但却要开辟 60 块不同的内存空间,这就会造成不必要的浪费。此时,原型对象就可以帮助我们解决这个问题。

2.如何使用原型对象

当一个函数 (注意:不仅仅只有构造函数) 创建好之后,都会有一个 prototype 属性,这个属性的值是一个对象,我们把这个对象,称为原型对象。同时,只要在这个原型对象上添加属性和方法,这些属性和方法都可以被该函数的实例所访问
在这里插入图片描述
既然,函数的实例可以访问到原型对象上的属性和方法,那我们不妨把上面的代码改造一下:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.say = function() {
    console.log('Hello');
};

var p1 = new Person('Tom', 18);
var p2 = new Person('Jack', 34);

console.log(p1.say === p2.say); // true

此时,我们看到实例 p1 和 实例 p2 的 say 指向同一块内存空间。这是什么原因呢?我们通过控制台的打印结果来看看。
在这里插入图片描述
通过上面的截图我们可以看到,Person.prototype 与 p1._proto 、p2._proto 似乎是一样的。为了验证我们的猜想,我们试着在打印:

Person.prototype === p1.__proto__;   // true
Person.prototype === p2.__proto__;   // true
p1.__proto__ === p2.__proto___;      // true

我们发现,所有的结果都为 true 。 而这正好解释了为什么 p1.say === p2.say 为 true 。

3.绘制 构造函数——原型对象——实例 关系图

我们就以下面的代码为例:

function Person(name) {
    this.name = name;
}

Person.prototype.say = function() {
    console.log('I am saying');
}

var p1 = new Person('Tom');
  1. Person 函数创建之后,会产生一块内存空间,并且有一个 prototype 属性
    在这里插入图片描述
  2. prototype 属性的值是一个对象,我们称之为原型对象
    在这里插入图片描述
  3. 原型对象中的属性和方法
    参照上面控制台的截图,我们可以知道:
    (1)原型对象上,有一个 constructor 属性指向 Person;
    (2)原型对象上,有一个 say 方法,会开辟一块新的内存空间;
    (3)原型对象上,有一个 _proto 属性
    根据上面我们的分析,继续绘制:
    在这里插入图片描述
  4. 实例中的属性和方法
    当 p1 这个实例创建好之后,又会开辟一块新的内存空间。此时,依旧参照上面控制台的截图,我们可以知道:
    (1)p1 实例中有一个 name 属性;
    (2)p1 实例中有一个 _proto 属性,指向构造函数 Person 的原型对象
    根据上面的分析,我们继续绘制:
    在这里插入图片描述
四、总结

通过上面的解释,大家应该可以理解原型对象是什么以及为什么要使用原型对象了。最后,我们来总结一下本文的核心知识点。
1.每个函数创建好之后(函数对象),就会有一个 prototype 属性,这个属性的值是一个对象,我们把这个 prototype 属性所指向的内存空间称为这个函数的原型对象。
2.某个函数的原型对象会有一个 constructor 属性,这个属性指向该函数本身。

function Person() {
    // ...
}
console.log(Person.prototype.constructor === Person); // true

3.当某个函数当成构造函数来调用时,就会产生一个构造函数的实例。这个实例上会拥有一个 _proto 属性,这个属性指向该实例的构造函数的原型对象(也可以称为该实例的原型对象),即每个对象都有 _proto 属性,但只有函数对象才有 prototype 属性。

function Person() {
    // ...
}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); // true

4.原型对象是一个普通对象。
原型对象其实就是普通对象(但 Function.prototype 除外,它是函数对象,但它很特殊,他没有prototype属性(前面说道函数对象都有prototype属性))。看下面的例子:

 function Person(){};
 console.log(Person.prototype) //Person{}
 console.log(typeof Person.prototype) //Object
 console.log(typeof Function.prototype) // Function,这个特殊
 console.log(typeof Object.prototype) // Object
 console.log(typeof Function.prototype.prototype) //undefined

Function.prototype 为什么是函数对象呢?

 var A = new Function ();
 Function.prototype = A;

上文提到凡是通过 new Function( ) 产生的对象都是函数对象。因为 A 是函数对象,所以Function.prototype 是函数对象。

原型链

当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去该对象的原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object原型对象的原型对象不存在,如果在Object原型对象中依然没有找到,则返回undefined。该寻找过程形成了一个原型指向的链条,专业术语称之为原型链。
在这里插入图片描述

一些问题

1.person1.__proto__ 是什么?
2.Person.__proto__ 是什么?
3.Person.prototype.__proto__ 是什么?
4.Object.__proto__ 是什么?
5.Object.prototype__proto__ 是什么?

1.person1.__proto__ === Person.prototype
2.Person.__proto__ === Function.prototype
3.Person.prototype 是一个普通对象,我们无需关注它有哪些属性,只要记住它是一个普通对象。
因为一个普通对象的构造函数 === Object
所以 Person.prototype.__proto__ === Object.prototype
4.Function.__proto__ === Function.prototype
5.Object.prototype.__proto__ === null

疑点解惑
1.Object.__proto__ === Function.prototype // true
Object 是函数对象,是通过new Function()创建的,所以Object.__proto__指向Function.prototype。(所有函数对象的__proto__都指向Function.prototype
2.Function.__proto__ === Function.prototype // true
Function 也是对象函数,也是通过new Function()创建,所以Function.__proto__指向Function.prototype。
3.Function.prototype.__proto__ === Object.prototype //true
其实这一点我也有点困惑,不过也可以试着解释一下。
Function.prototype是个函数对象,理论上他的__proto__应该指向 Function.prototype,就是他自己,自己指向自己,没有意义。
JS一直强调万物皆对象,函数对象也是对象,给他认个祖宗,指向Object.prototype。Object.prototype.__proto__ === null,保证原型链能够正常结束。

原型和原型链是JS实现继承的一种模型。
原型链的形成是真正是靠__proto__ 而非prototype

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值