一、为什么要使用原型?怎样去理解原型的出现

1、对象字面量创建对象的缺点

想要介绍原型,就不得不提为什么我们要使用原型,在js早期,我们创建一个对象,比较流行的做法是使用对象字面量去创建一个对象,例如:

const person = {
            name: "wywy",
            age: 21,
            hobby: "听周杰伦"
}

用这种方式去创建对象,虽然简洁明了,但是我们如果需要大批量的创建这一类的对象,就像person这个对象,我们可能需要去创建多个不同的人,那么每一次都需要去声明创建,这样工作量是巨大的,为了解决这个问题我们引入了工厂函数的概念。

2、工厂函数

什么是工厂函数,顾名思义就可以把工厂函数看作是一个流水线的工厂,这个工厂的作用就是批量生产person对象,就像下面这样:

function createPerson(name,age,hobby){
	const obj = {};
	obj.name = name;
	obj.age = age;
	obj.hobby = hobby;
	return obj;
}
const person1 = createPerson("zs", 23, "滑板");
const person2 = createPerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);

在这里插入图片描述

这里我们在创建person对象的时候只需要调用这个函数即可,并把每个对象对应的属性值传入就好了,这样相对于用对象字面量去创建一个个的person确实简化了代码量,但是工厂函数也有自身的缺点,就是我们不能去判断出这个对象的类型,按我们知道的在 js 中复杂引用数据类型进行细分,有 Array,Function,String 等,但是通过工厂函数模式创建的这些对象用控制台打印去全部都是 Object 类型,假如你有多个工厂函数,用多个工厂函数创建了多个实例,但是你却并不知道这些对象属于哪个工厂。为此又推出了构造函数这个概念。

3、构造函数

关于构造函数其实它和工厂函数非常相似,在js的函数中其实并没有单独的一类函数叫做构造函数,构造函数总是和new关键字一起使用,更准确地说一个函数被构造调用了。当一个构造函数不使用new关键字调用时,他和普通的函数无异。

function CreatePerson(name, age, hobby) {
            this.name = name;
            this.age = age;
            this.hobby = hobby;
}
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
console.log(person1);
console.log(person2);

在这里插入图片描述

仔细观察,不难发现我们对上面的工厂函数进行了以下几点改造:

1、我们将函数名的首字母大写了,在 js 中有个规定如果你以后打算将一个函数作为构造函数去使用那么最好把它的函数名首字母大写,来提醒使用者这是一个构造函数。其实不大写也不会有什么语法错误。

2、我们取消了在函数内部显示的声明一个对象 “const obj = {}”,并一并取消了最后返回这个对象的操作 “return obj”。

3、在调用这个这个 CreatePerson 函数时在前面加上了 new 关键字。

在这里插入图片描述

然后我们看这个结果,这个时候我们打印的对象不在是 Object 了,而是我们自己创建的函数 CreatePerson。看来构造函数确实解决了对象无法判定类型的问题。
那么神奇的new关键字在后台做了什么呢?其实它做了下面的五件事

1、在内存中创建一个新的对象。

2、让新对象的内部特性 [[Prototype]] 保存函数 CreatePerson 的 prototype 的属性值,也就是把函数的原型的指针保存到了 [[Prototype]] 中。

3、把函数内部的 this 指向这个新创建的对象。

4、执行函数内部的代码。

5、如果函数本身没有返回对象,那么就把这个新对象返回。

我们看构造函数原来在后台为我们做了这么多事。那么构造函数就完美了吗?并不是的,我们想一个 person 是不是应该也该给他加一个方法呢?那么我们就给加一个说话的方法吧:

function CreatePerson(name, age, hobby) {
     this.name = name;
     this.age = age;
     this.hobby = hobby;
     // 添加一个方法
     this.sayHi = function() {
         console.log("你好,我叫" + this.name)
     }
 }
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
 console.log(person1);
 console.log(person2);

但是我们发现这样做无疑为每一个实例对象都添加了一个 sayHi 方法,而且每个实例上的方法都不相等,但我们的目的是让每个实例都有这么一个功能就好了,不必创建这么多的 sayHi 方法而去浪费内存空间。说的直白一点,比如家里有五个孩子,每个孩子都想玩 switch 游戏,那么家长要给每个孩子买一台 switch 吗?当然家里有矿的当我没说,一般家庭是不是就买一台,然后哪个孩子想玩就管家长去要就行了是不是。

同样的,代码就是对现实生活的抽象,那么我们是不是也可以这样做,把方法添加到这些实例都拥有的一个爸爸是不是就好了,而仔细想想在 new 的五步中第二步是不是做了这么一件事,没错他就是 js 中的原型。这个原型就是这些实例的爸爸。

说了这么多我们总算要讲原形了!!!

二、使用原型

通过上面的讲解我们似乎对原型的作用有了大致的理解,就是把实例对象上需要用到的共有方法添加到原型上,而实例对象的自己的私有属性写在构造函数内部。
接下来我们对构造函数进行再一次改造:

function CreatePerson(name, age, hobby) {
     this.name = name;
     this.age = age;
     this.hobby = hobby;
     // 添加一个方法
     this.sayHi = function() {
         console.log("你好,我叫" + this.name)
     }
 }
const person1 = new CreatePerson("zs", 23, "滑板");
const person2 = new CreatePerson("ls", 22, "听歌");
 console.log(person1);
 console.log(person2);

首先向大家说明一点我并没有按着定义一个构造函数,然后在构造函数的原型上添加 sayHi 方法,接着使用 new 创建实例的顺序来写代码。而是先 new 创建了实例,然后再在原型上添加方法,这样的目的是想告诉大家,原型是具有动态性的,即你先创建了实例,在实例之后给原型添加了方法那么实例依然是可以访问的。而且可以看到通过比较 person1 和 person2 的 sayHi 方法我们发现这是同一个方法。这样我们完美的解决了构造函数的问题。

三、原型概念辨析

首先清楚两个概念:

  • 引用类型,都具有对象特性,即可自由扩展属性。(引用类型:Object、Array、Function、Date、RegExp)
  • 每个函数function都有一个显示原型prototype,每个实例对象都有一个隐式原型__proto__
function Fn() { // 内部语句:this.prototype = {}
}
// 1、每个函数function都有一个prototype,即显示原型(属性)
console.log(Fn.prototype);
// 2、每个实例对象都有一个__proto__,可称为隐式原型(属性)
var fn = new Fn(); // 内部语句: this.__proto__ = Fn.prototype
console.log(fn.__proto__);

两个准则:

在设计js原型原型链的时候遵循以下两个准则:

准则一: 原型对象(即Fn.prototype)的 constructor 指向构造函数本身。

准则二: 实例对象(即 fn )的__proto__ 指向其构造函数的显示原型。

function Fn() {}
var fn = new Fn();
// 原型对象的 constructor 指向构造函数本身
console.log(Fn.prototype.constructor === Fn); // true
// 对象的隐式原型的值为其对应构造函数的显示原型的值
console.log(Fn.prototype === fn.__proto__); // true

理解Function与Object特例

每个函数都是 Function 的实例,所以每个函数既有显示原型又有隐式原型,所有函数的隐式原型指向 Function.prototype; 构造器Function的构造器是它自身。

// function Foo() {} 相当于 var Foo = new Function()
// Function = new Function() => Function.__proto__ = Function.prototype
// Object 作为构造函数时,其 __proto__ 内部属性值指向 Function.prototype
// Object.__proto__ = Function.prototype
// Function.constructor=== Function;//true

Object构造函数创建一个对象包装器。JavaScript中的所有对象都来自 Object,所有对象都是Object的实例;所有对象从Object.prototype继承方法和属性,尽管它们可能被覆盖。

// Fn的原型对象(Fn.prototype)也来自Object,故Fn.prototype.__proto__ = Object.prototype
function Fn() {}

原型链:

读取某个对象的属性时,会自动找到原型链中查找。

  • 1、现在自身属性中查找,找到返回
  • 2、找不到则继续沿着__proto__这条链向上查找,找到返回
  • 3、如果最终没有找到。返回undefined设置对象的属性值时,不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值方法一般定义在原型中,属性一般通过构造函数定义在对象本身

原型链就是一个过程,原型是原型链这个过程中的一个单位,贯穿整个原型链

图解

在这里插入图片描述

四、原型链练习

//练习题1
function A(){
}
A.prototype.n = 1;
var b = new A();
A.prototype = {
	n:2,
	m:3
}
var c = new A();
console.log(b.n,b.m,c.n,c.m)
// 1 undefined 2 3
// 测试题2
var F = function() {
};
Object.prototype.a = function() {
    console.log('a()');
};
Function.prototype.b = function() {
    console.log('b()');
};
var f = new F();
f.a(); // a()
f.b(); // 报错:Uncaught TypeError: f.b is not a function
F.a(); // a()
F.b(); // b()

原型、原型链的意义与使用场景:

原型对象的作用,是用来存放实例中共有的那部分属性、方法、可以大大减少内存消耗。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注阿兔在线工具的更多内容!       

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部