一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
上一篇我们讲了面向对象的具体含义,其中包括了class的相关概念,那么这一篇文章我们来继续深入的理解一下class中的其他奥妙。
首先 class 并不是标签上的属性,而是 javascript中关于模块定义的一个语法糖,我们先来看看官方定义
类是用于创建对象的模板。他们用代码封装数据以处理该数据。 JS中的类建立在原型上,但也具有某些语法和语义未与ES5类相似语义共享。
它就是一个特殊的函数,只不过定义的方式不同,关于类的定义和关键字的说明请我的上一篇文章 面向对象编程,你真正懂吗
好的费话不多少,开始讲解
ES6中的类和对象
构造函数和原型
在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到函数里面。
在JS 中,使用构造函数时要注意以下两点:
-
构造函数用于创建某一类对象,其首字母要大写
-
构造函数要和new 一起使用才有意义
这里有个很重要的考点new在执行时会做四件事情
- 在内存中创建一个新的空对象。
- 让
this指向这个新的对象。 - 执行构造函数里面的代码,给这个新对象添加属性和方法。
- 返回这个新对象(所以构造函数里面不需要
return)。
静态成员和实例成员
JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。
- 静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问
举个🌰
function Person (uname, age) {
this.uname = uname;
this.age = age;
this.say = function () {
console.log(123);
}
}
var obj = new Person('张三丰',22);
console.log(obj.uname); // 张三丰
console.log( Person.uname ); // undefined
Person.leibie = '人';
console.log(Person.leibie); // 人
console.log(obj.leibie); // undefined
这里有个小疑问
当实例化对象的时候,属性好理解,属性名属性值,那么方法是函数,函数是复杂数据类型。那么保存的时候是保存地址,又指向函数,而每创建一个对象,都会有一个函数,每个函数都得开辟一个内存空间,此时浪费内存了,那么如何节省内存呢?
这时我们就需要用到原型
构造函数原型 prototype
什么是原型对象:就是一个属性,是构造函数的属性,这个属性是一个对象,我们也称呼,prototype 为原型对象。
作用:是为了共享方法,从而达到节省内存
注意:每一个构造函数都有prototype属性
构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。我们可以把那些不变的方法,直接定义在prototype 对象上,这样所有对象的实例就可以共享这些方法。
function Star (uname, age) {
this.uname = uname;
this.age = age;
// this.sing = function () {
// console.log(this.name + '在唱歌');
// }
}
Star.prototype.sing = function () {
console.log(this.uname + '在唱歌');
}
var zxc = new Star('周星驰', 22);
var ldh = new Star('刘德华', 22);
// console.log( Star.prototype );
ldh.sing();
zxc.sing();
总结:所有的公共属性写到构造函数里面,所有的公共方法写到原型对象里面
又有一个疑问:为何创建一个对象,都可以自动的跑到原型对象上找方法
因为每一个对象都有一个属性,proto 对象原型,执行原型对象
对象原型:__proto __
主要作用:指向prototype
构造函数和原型对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。
注意:
- ____proto____是一个非标准属性,不可以拿来赋值或者设置【只读属性】
- ____proto____对象原型和原型对象prototype 是等价的
- ____proto____对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype
总结:每一个对象都有一个原型,作用是指向原型对象prototype
统一称呼:proto原型,prototype成为原型对象
构造函数 constructor
记录是哪个构造函数创建出来的
指回构造函数本身
原型 proto 和构造函数 prototype 原型对象里面都有一个属性constructor,constructor 我们称为构造函数,因为它指回构造函数本身。constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个constructor 指向原来的构造函数。
总结:constructor 主要作用可以指回原来的构造函数
看到这是不是已经乱了, 那么接下来就说一下它们之间的关系到底是怎样的相辅相成的
构造函数、实例、原型对象三者之间的关系
原型链
作用:提供一个成员的查找机制,或者查找规则
原型链:查找属性或者方法的时候,由_____proto____组成的一条链
总结
-
构造函数:
- 方法和属性:
- 构造函数里面放属性,
- 原型对象里面放方法
- 方法和属性:
-
原型对象:是构造函数的一个属性,【每一个构造函数都有原型对象】,作用:放方法【prototype】
-
对象原型:是对象的一个属性,【每一个对象都有对象原型】,作用:指向原型对象【__proto __】
-
构造函数:constructor:是原型对象的一个属性,作用:指回构造函数
-
原型链:原型组成的一个查找成员机制的链(路线),作用:查找成员的机制
JavaScript 的成员查找机制(规则)
当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是__proto__指向的prototype 原型对象)。
如果还没有就查找原型对象的原型(Object的原型对象)。
依此类推一直找到Object 为止(null)。
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
console.log(Star.prototype.__proto__.__proto__);
console.log(Object.prototype);
扩展内置对象
可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。
扩展:最大值最小值
console.log( Array.prototype );
// 添加求和方法
Array.prototype.sum = function () {
var sum = 0;
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
var arr = [1,2,3];
console.log( arr.sum() );
var newArr = [6,7,8,9];
console.log( newArr.sum() );
继承
关于继承的方法有很多,网上的整理也比较全面,本小节只谈论class中的继承
ES6之前并没有给我们提供extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
call()
调用这个函数, 并且修改函数运行时的this 指向
fun.call(thisArg, arg1, arg2, ...);call把父类的this指向子类
thisArg :当前调用函数this 的指向对象
arg1,arg2:传递的其他参数
属性的继承
利用构造函数实现属性的继承
function Father (uname,age) {
// this指向父类的实例对象
this.uname = uname;
this.age = age;
// 只要把父类的this指向子类的this既可
}
function Son (uname, age,score) {
// this指向子类构造函数
// this.uname = uname;
// this.age = age;
// Father(uname,age);
Father.call(this,uname,age);
this.score = score;
}
Son.prototype.sing = function () {
console.log(this.uname + '唱歌')
}
var obj = new Son('刘德华',22,99);
console.log(obj.uname);
console.log(obj.score);
obj.sing();
方法的继承
实现方法把父类的实例对象保存给子类的原型对象
一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。核心原理:
①将子类所共享的方法提取出来,让子类的prototype 原型对象= new 父类()
②本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
③将子类的constructor 指回本身
function Father () {
}
Father.prototype.chang = function () {
console.log('唱歌');
}
function Son () {
}
// Son.prototype = Father.prototype;
Son.prototype = new Father();
var obj = new Son();
obj.chang();
Son.prototype.score = function () {
console.log('考试');
}
// obj.score();
// console.log(Son.prototype);
console.log(Father.prototype);
注意:一定要让Son指回构造函数
实现继承后,让Son指回原构造函数
Son.prototype = new Father();
Son.prototype.constructor = Son;
总结:用构造函数实线属性继承,用原型对象实线方法继承
类的本质
class本质还是function
类的所有方法都定义在类的prototype属性上
类创建的实例,里面也有__proto__指向类的prototype原型对象
所以ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
所以ES6的类其实就是语法糖.
语法糖:语法糖就是一种便捷写法. 简单理解, 有两种方法可以实现同样的功能, 但是一种写法更加清晰、方便,那么这个方法就是语法糖