前端基础进阶(十一):详解面向对象、构造函数、原型与原型链

name: ‘TOM’,

age: 20,

getName: function() {

return this.name

}

};

var perJake = {

name: ‘Jake’,

age: 22,

getName: function() {

return this.name

}

}

很显然这并不是合理的方式,当相似对象太多时,大家都会崩溃掉。

可以使用工厂模式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出需要的对象。我们需要多少个,就复制多少个。

var createPerson = function(name, age) {

// 声明一个中间对象,该对象就是工厂模式的模子

var o = new Object();

// 依次添加我们需要的属性与方法

o.name = name;

o.age = age;

o.getName = function() {

return this.name;

}

return o;

}

// 创建两个实例

var perTom = createPerson(‘TOM’, 20);

var PerJake = createPerson(‘Jake’, 22);

相信上面的代码并不难理解,也不用把工厂模式看得太过高大上。很显然,工厂模式帮助我们解决了重复代码上的麻烦,让我们可以写很少的代码,就能够创建很多个person对象。但是这里还有两个麻烦,需要我们注意。

第一个麻烦就是这样处理,我们没有办法识别对象实例的类型。使用instanceof可以识别对象的类型,如下例子:

var obj = {};

var foo = function() {}

console.log(obj instanceof Object); // true

console.log(foo instanceof Function); // true

因此在工厂模式的基础上,我们需要使用构造函数的方式来解决这个麻烦。

三、构造函数

在JavaScript中,new关键字可以让一个函数变得与众不同。通过下面的例子,我们来一探new关键字的神奇之处。

function demo() {

console.log(this);

}

demo(); // window

new demo(); // demo

为了能够直观的感受他们不同,建议大家动手实践观察一下。很显然,使用new之后,函数内部发生了事情,让this指向改变。

new关键字到底做了什么?之前在文章里我用文字大概表达了一下new的作用,但是一些同学好奇心很足,总期望用代码实现一下,我就大概以我的理解来表达一下吧。

// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别

var Person = function(name, age) {

this.name = name;

this.age = age;

this.getName = function() {

return this.name;

}

}

// 将构造函数以参数形式传入

function New(func) {

// 声明一个中间对象,该对象为最终返回的实例

var res = {};

if (func.prototype !== null) {

// 将实例的原型指向构造函数的原型

res.proto = func.prototype;

}

// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象

var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));

// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象

if ((typeof ret === “object” || typeof ret === “function”) && ret !== null) {

return ret;

}

// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象

return res;

}

// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res

var p1 = New(Person, ‘tom’, 20);

console.log(p1.getName());

// 当然,这里也可以判断出实例的类型了

console.log(p1 instanceof Person); // true

JavaScript内部再通过其他的一些特殊处理,将var p1 = New(Person, 'tom', 20); 等效于var p1 = new Person('tom', 20);。就是我们认识的new关键字了。具体怎么处理的,我也不知道,别刨根问底了,一直回答下去我太难了 - -!

老实讲,你可能很难在其他地方看到有如此明确的告诉你new关键字到底对构造函数干了什么的文章了。理解了这段代码,你对JavaScript的理解又比别人深刻了一分,所以,一本正经厚颜无耻求个赞可好?

当然,很多朋友由于对于前面几篇文章的知识理解不够到位,会对new的实现表示非常困惑。但是老实讲,如果你读了我的前面几篇文章,一定会对这里new的实现有似曾相识的感觉。而且我这里已经尽力做了详细的注解,剩下的只能靠你自己了。

但是只要你花点时间,理解了他的原理,那么困扰了无数人的构造函数中this到底指向谁就变得非常简单了。

所以,为了能够判断实例与对象的关系,我们就使用构造函数来搞定。

var Person = function(name, age) {

this.name = name;

this.age = age;

this.getName = function() {

return this.name;

}

}

var p1 = new Person(‘Ness’, 20);

console.log(p1.getName()); // Ness

console.log(p1 instanceof Person); // true

关于构造函数,如果你暂时不能够理解new的具体实现,就先记住下面这几个结论吧。

  • 与普通函数相比,构造函数并没有任何特别的地方,首字母大写只是我们约定的小规定,用于区分普通函数;

  • new关键字让构造函数具有了与普通函数不同的许多特点,而new的过程中,执行了如下过程:

  1. 声明一个中间对象;

  2. 将该中间对象的原型指向构造函数的原型;

  3. 将构造函数的this,指向该中间对象;

  4. 返回该中间对象,即返回实例对象。

四、原型

虽然构造函数解决了判断实例类型的问题,但是,说到底,还是一个对象的复制过程。跟工厂模式颇有相似之处。也就是说,当我们声明了100个person对象,那么就有100个getName方法被重新生成。

这里的每一个getName方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为getName分配空间。这就是工厂模式存在的第二个麻烦。

显然这是不合理的。我们期望的是,既然都是实现同一个功能,那么能不能就让每一个实例对象都访问同一个方法?

当然能,这就是原型对象要帮我们解决的问题了。

我们创建的每一个函数,都可以有一个prototype属性,该属性指向一个对象。这个对象,就是我们这里说的原型。

当我们在创建对象时,可以根据自己的需求,选择性的将一些属性和方法通过prototype属性,挂载在原型对象上。而每一个new出来的实例,都有一个__proto__属性,该属性指向构造函数的原型对象,通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当所有的实例都能够通过__proto__访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。

我们通过一个简单的例子与图示,来了解构造函数,实例与原型三者之间的关系。

由于每个函数都可以是构造函数,每个对象都可以是原型对象,因此如果在理解原型之初就想的太多太复杂的话,反而会阻碍你的理解,这里我们要学会先简化它们。就单纯的剖析这三者的关系。

// 声明构造函数

function Person(name, age) {

this.name = name;

this.age = age;

}

// 通过prototye属性,将方法挂载到原型对象上

Person.prototype.getName = function() {

return this.name;

}

var p1 = new Person(‘tim’, 10);

var p2 = new Person(‘jak’, 22);

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

图示

通过图示我们可以看出,构造函数的prototype与所有实例对象的__proto__都指向原型对象。而原型对象的constructor指向构造函数。

除此之外,还可以从图中看出,实例对象实际上对前面我们所说的中间对象的复制,而中间对象中的属性与方法都在构造函数中添加。于是根据构造函数与原型的特性,我们就可以将在构造函数中,通过this声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。而通过原型声明的属性与方法,我们可以称之为共有属性与方法,它们可以被所有的实例对象访问。

当我们访问实例对象中的属性或者方法时,会优先访问实例对象自身的属性和方法。

function Person(name, age) {

this.name = name;

this.age = age;

this.getName = function() {

console.log(‘this is constructor.’);

}

}

Person.prototype.getName = function() {

return this.name;

}

var p1 = new Person(‘tim’, 10);

p1.getName(); // this is constructor.

在这个例子中,我们同时在原型与构造函数中都声明了一个getName函数,运行代码的结果表示原型中的访问并没有被访问。

我们还可以通过in来判断,一个对象是否拥有某一个属性/方法,无论是该属性/方法存在于实例对象还是原型对象。

function Person(name, age) {

this.name = name;

this.age = age;

}

Person.prototype.getName = function() {

return this.name;

}

var p1 = new Person(‘tim’, 10);

console.log(‘name’ in p1); // true

in的这种特性最常用的场景之一,就是判断当前页面是否在移动端打开。

isMobile = ‘ontouchstart’ in document;

// 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式

更简单的原型写法

根据前面例子的写法,如果我们要在原型上添加更多的方法,可以这样写:

function Person() {}

Person.prototype.getName = function() {}

Person.prototype.getAge = function() {}

Person.prototype.sayHello = function() {}

… …

除此之外,我还可以使用更为简单的写法。

function Person() {}

Person.prototype = {

constructor: Person,

getName: function() {},

getAge: function() {},

sayHello: function() {}

}

这种字面量的写法看上去简单很多,但是有一个需要特别注意的地方。Person.prototype = {}实际上是重新创建了一个{}对象并赋值给Person.prototype,这里的{}并不是最初的那个原型对象。因此它里面并不包含constructor属性。为了保证正确性,我们必须在新创建的{}对象中显示的设置constructor的指向。即上面的constructor: Person

五、原型链

原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。因此理解了原型,那么原型链并不是一个多么复杂的概念。

我们知道所有的函数都有一个叫做toString的方法。那么这个方法到底是在哪里的呢?

先随意声明一个函数:

function add() {}

那么我们可以用如下的图来表示这个函数的原型链。

原型链

其中add是Function对象的实例。而Function的原型对象同时又是Object的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。

基于原型链的特性,我们可以很轻松的实现继承

六、继承

我们常常结合构造函数与原型来创建一个对象。因为构造函数与原型的不同特性,分别解决了我们不同的困扰。因此当我们想要实现继承时,就必须得根据构造函数与原型的不同而采取不同的策略。

我们声明一个Person对象,该对象将作为父级,而子级cPerson将要继承Person的所有属性与方法。

function Person(name, age) {

this.name = name;

this.age = age;

}

Person.prototype.getName = function() {

return this.name;

}

首先我们来看构造函数的继承。在上面我们已经理解了构造函数的本质,它其实是在new内部实现的一个复制过程。而我们在继承时想要的,就是想父级构造函数中的操作在子级的构造函数中重现一遍即可。我们可以通过call方法来达到目的。

// 构造函数的继承

function cPerson(name, age, job) {

Person.call(this, name, age);

this.job = job;

}

原型的继承,只需要将子级的原型对象设置为父级的一个实例,加入到原型链中即可。

// 继承原型

cPerson.prototype = new Person(name, age);

// 添加更多方法

cPerson.prototype.getLive = function() {}

原型链

当然关于继承还有更好的方式。

七、更好的继承

假设原型链的终点Object.prototype为原型链的E(end)端,原型链的起点为S(start)端。

通过前面原型链的学习我们知道,处于S端的对象,可以通过S -> E的单向查找,访问到原型链上的所有方法与属性。因此这给继承提供了理论基础。我们只需要在S端添加新的对象,那么新对象就能够通过原型链访问到父级的方法与属性。因此想要实现继承,是一件非常简单的事情。

因为封装一个对象由构造函数与原型共同组成,因此继承也会分别有构造函数的继承与原型的继承。

假设我们已经封装好了一个父类对象Person。如下。

var Person = function(name, age) {

this.name = name;

this.age = age;

}

Person.prototype.getName = function() {

return this.name;

}

Person.prototype.getAge = function() {

return this.age;

}

构造函数的继承比较简单,我们可以借助call/apply来实现。假设我们要通过继承封装一个Student的子类对象。那么构造函数可以如下实现。

var Student = function(name, age, grade) {

// 通过call方法还原Person构造函数中的所有处理逻辑

Person.call(this, name, age);

this.grade = grade;

}

// 等价于

var Student = function(name, age, grade) {

this.name = name;

this.age = age;

this.grade = grade;

}

原型的继承则稍微需要一点思考。首先我们应该考虑,如何将子类对象的原型加入到原型链中?我们只需要让子类对象的原型,成为父类对象的一个实例,然后通过__proto__就可以访问父类对象的原型。这样就继承了父类原型中的方法与属性了。

因此我们可以先封装一个方法,该方法根据父类对象的原型创建一个实例,该实例将会作为子类对象的原型。

function create(proto, options) {

// 创建一个空对象

var tmp = {};

// 让这个新的空对象成为父类对象的实例

tmp.proto = proto;

// 传入的方法都挂载到新对象上,新的对象将作为子类对象的原型

Object.defineProperties(tmp, options);

return tmp;

}

简单封装了create对象之后,我们就可以使用该方法来实现原型的继承了。

Student.prototype = create(Person.prototype, {

// 不要忘了重新指定构造函数

constructor: {

value: Student

}

getGrade: {

value: function() {

return this.grade

}

}

})

那么我们来验证一下我们这里实现的继承是否正确。

var s1 = new Student(‘ming’, 22, 5);

console.log(s1.getName()); // ming

console.log(s1.getAge()); // 22

console.log(s1.getGrade()); // 5

全部都能正常访问,没问题。在ECMAScript5中直接提供了一个Object.create方法来完成我们上面自己封装的create的功能。因此我们可以直接使用Object.create.

Student.prototype = create(Person.prototype, {

// 不要忘了重新指定构造函数

constructor: {

value: Student

}

getGrade: {

value: function() {

return this.grade

}

}

})

完整代码如下:

function Person(name, age) {

this.name = name;

this.age = age;

}

Person.prototype.getName = function() {

return this.name

}

Person.prototype.getAge = function() {

return this.age;

}

function Student(name, age, grade) {

// 构造函数继承

Person.call(this, name, age);

this.grade = grade;

}

// 原型继承

Student.prototype = Object.create(Person.prototype, {

// 不要忘了重新指定构造函数

constructor: {

value: Student

}

getGrade: {

value: function() {

return this.grade

}

}

})

var s1 = new Student(‘ming’, 22, 5);

console.log(s1.getName()); // ming

console.log(s1.getAge()); // 22

console.log(s1.getGrade()); // 5

八、属性类型

在上面的继承实现中,使用了一个大家可能不太熟悉的方法defineProperties。并且在定义getGrade时使用了一个很奇怪的方式。

Vue 面试题

1.Vue 双向绑定原理
2.描述下 vue 从初始化页面–修改数据–刷新页面 UI 的过程?
3.你是如何理解 Vue 的响应式系统的?
4.虚拟 DOM 实现原理
5.既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异?
6.Vue 中 key 值的作用?
7.Vue 的生命周期
8.Vue 组件间通信有哪些方式?
9.watch、methods 和 computed 的区别?
10.vue 中怎么重置 data?
11.组件中写 name 选项有什么作用?
12.vue-router 有哪些钩子函数?
13.route 和 router 的区别是什么?
14.说一下 Vue 和 React 的认识,做一个简单的对比
15.Vue 的 nextTick 的原理是什么?
16.Vuex 有哪几种属性?
17.vue 首屏加载优化
18.Vue 3.0 有没有过了解?
19.vue-cli 替我们做了哪些工作?

算法

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  1. 冒泡排序
  2. 选择排序
  3. 快速排序
  4. 二叉树查找: 最大值、最小值、固定值
  5. 二叉树遍历
  6. 二叉树的最大深度
  7. 给予链表中的任一节点,把它删除掉
  8. 链表倒叙
  9. 如何判断一个单链表有环
  10. 给定一个有序数组,找出两个数相加为一个目标数

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+评论666

e.log(s1.getGrade()); // 5

八、属性类型

在上面的继承实现中,使用了一个大家可能不太熟悉的方法defineProperties。并且在定义getGrade时使用了一个很奇怪的方式。

Vue 面试题

1.Vue 双向绑定原理
2.描述下 vue 从初始化页面–修改数据–刷新页面 UI 的过程?
3.你是如何理解 Vue 的响应式系统的?
4.虚拟 DOM 实现原理
5.既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异?
6.Vue 中 key 值的作用?
7.Vue 的生命周期
8.Vue 组件间通信有哪些方式?
9.watch、methods 和 computed 的区别?
10.vue 中怎么重置 data?
11.组件中写 name 选项有什么作用?
12.vue-router 有哪些钩子函数?
13.route 和 router 的区别是什么?
14.说一下 Vue 和 React 的认识,做一个简单的对比
15.Vue 的 nextTick 的原理是什么?
16.Vuex 有哪几种属性?
17.vue 首屏加载优化
18.Vue 3.0 有没有过了解?
19.vue-cli 替我们做了哪些工作?

[外链图片转存中…(img-4VRK3GHq-1714568126349)]

算法

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  1. 冒泡排序
  2. 选择排序
  3. 快速排序
  4. 二叉树查找: 最大值、最小值、固定值
  5. 二叉树遍历
  6. 二叉树的最大深度
  7. 给予链表中的任一节点,把它删除掉
  8. 链表倒叙
  9. 如何判断一个单链表有环
  10. 给定一个有序数组,找出两个数相加为一个目标数

    [外链图片转存中…(img-nXeqfVrJ-1714568126350)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+评论666

  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值