类是用于创建对象的模板。JavaScript中生成对象实例的方法是通过构造函数,这跟主流面向对象语言(java,C#)写法上差异较大,如下:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 1);
ES6 提供了更接近Java语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
如下:constructor()是构造方法,而this代表实例对象:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
类的数据类型就是函数,它本身就是指向函数的构造函数:
// ES5 函数声明
function Point() {
//...
}
// ES6 类声明
class Point {
//....
constructor() {
}
}
typeof Point // "function"
Point === Point.prototype.constructor // true
在类里面定义的方法是挂到Point.prototype,所以类只是提供了语法糖,本质还是原型链调用。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
Point.prototype = {
//....
toString()
}
var p = new Point(1, 1);
p.toString() // (1,1)
类的另一种定义方式类表达式
// 未命名/匿名类
let Point = class {
constructor(x, y) {
this.x = x;
this.y = y;
}
};
Point.name // Point
函数声明和类声明有个重要区别,函数声明会提升,类声明不会提升。
constructor()方法是类的默认方法,new生成实例对象时会自动调用该方法。
一个类必须有constructor()方法,如果没有显式定义,引擎会默认添加一个空的constructor()。
constructor()方法默认返回实例对象(即this)。
class Point {
}
// 自动添加
class Point {
constructor() {}
}
与 ES5 一样,在类的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class User {
constructor(name) {
this.name = name;
}
get name() {
return this.name;
}
set name(value) {
this.name = value;
}
}
类的方法内部的this,它默认指向类的实例,在调用存在this的方法时,需要使用 obj.method()方式,否则会报错。
class User {
constructor(name) {
this.name = name;
}
printName(){
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
user.printName() // Name is jack
const { printName } = user;
printName() // 报错 Cannot read properties of undefined (reading 'name')
如果要单独调用又不报错,一种方法可以在构造方法里调用bind(this)。
class User {
constructor(name) {
this.name = name;
this.printName = this.printName.bind(this);
}
printName(){
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
const { printName } = user;
printName() // Name is jack
bind(this) 会创建一个新函数,并将传入的this作为该函数在调用时上下文指向。
另外可以使用箭头函数,因为箭头函数内部的this总是指向定义时所在的对象。
class User {
constructor(name) {
this.name = name;
}
printName = () => {
console.log('Name is ' + this.name)
}
}
const user = new User('jack')
const { printName } = user;
printName() // Name is jack
静态属性指的是类本身的属性,而不是定义在实例对象this上的属性。
class User {
}
User.prop = 1;
User.prop // 1
可以在类里面定义静态方法,该方法不会被对象实例继承,而是直接通过类来调用。
静态方法里使用this是指向类。
class Utils {
static printInfo() {
this.info();
}
static info() {
console.log('hello');
}
}
Utils.printInfo() // hello
关于方法的调用范围限制,比如:私有公有,ES6暂时没有提供,一般是通过约定,比如:在方法前面加下划线_print()表示私有方法。
Java中通过extends实现类的继承。ES6中类也可以通过extends实现继承。
继承时,子类必须在constructor方法中调用super方法,否则新建实例时会报错。
class Point3D extends Point {
constructor(x, y, z) {
super(x, y); // 调用父类的constructor(x, y)
this.z = z;
}
toString() {
return super.toString() + ' ' + this.z ; // 调用父类的toString()
}
}
父类的静态方法,也会被子类继承。
class Parent {
static info() {
console.log('hello world');
}
}
class Child extends Parent {
}
Child.info() // hello world
在子类的构造函数必须执行一次super函数,它代表了父类的构造函数。
class Parent {}
class Child extends Parent {
constructor() {
super();
}
}
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
class Parent {
constructor() {
this.x = 1;
this.y = 10
}
printParent() {
console.log(this.y);
}
print() {
console.log(this.x);
}
}
class Child extends Parent {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let c = new Child();
c.printParent() // 10
c.m() // 2
初学JavaScript时,_proto_和prototype 很容易混淆。首先我们知道每个JS对象都会对应一个原型对象,并从原型对象继承属性和方法。
下图是一些拥有prototype内置对象。
prototype
根据上面描述,看下面代码
var obj = {} // 等同于 var obj = new Object()
// obj.__proto__指向Object构造函数的prototype
obj.__proto__ === Object.prototype // true
// obj.toString 调用方法从Object.prototype继承
obj.toString === obj.__proto__.toString // true
// 数组
var arr = []
arr.__proto__ === Array.prototype // true
对于function对象,声明的每个function同时拥有prototype和__proto__属性,创建的对象属性__proto__指向函数prototype,函数的__proto__又指向内置函数对象(Function)的prototype。
function Foo(){}
var f = new Foo();
f.__proto__ === Foo.prototype // true
Foo.__proto__ === Function.prototype // true
类作为构造函数的语法糖,也会同时有prototype属性和__proto__属性,因此同时存在两条继承链。
class Parent {
}
class Child extends Parent {
}
Child.__proto__ === Parent // true
Child.prototype.__proto__ === Parent.prototype // true
子类实例的__proto__属性,指向子类构造方法的prototype。
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
class Parent {
}
class Child extends Parent {
}
var p = new Parent();
var c = new Child();
c.__proto__ === p.__proto__ // false
c.__proto__ === Child.prototype // true
c.__proto__.__proto__ === p.__proto__ // true
JavaScript中的Class更多的还是语法糖,本质上绕不开原型链。欢迎大家留言交流。
上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
有没有更简单的写法?有!
新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。
我们先回顾用函数实现Student的方法:
function Student(name) { this.name = name; } Student.prototype.hello = function () { alert('Hello, ' + this.name + '!'); }
如果用新的class关键字来编写Student,可以这样写:
class Student { constructor(name) { this.name = name; } hello() { alert('Hello, ' + this.name + '!'); } }
比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。
最后,创建一个Student对象代码和前面章节完全一样:
var xiaoming = new Student('小明'); xiaoming.hello();
用class定义对象的另一个巨大的好处是继承更方便了。想一想我们从Student派生一个PrimaryStudent需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends来实现:
class PrimaryStudent extends Student { constructor(name, grade) { super(name); // 记得用super调用父类的构造方法! this.grade = grade; } myGrade() { alert('I am at grade ' + this.grade); } }
注意PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student。子类的构造函数可能会与父类不太相同,例如,PrimaryStudent需要name和grade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。
PrimaryStudent已经自动获得了父类Student的hello方法,我们又在子类中定义了新的myGrade方法。
ES6引入的class和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。
你一定会问,class这么好用,能不能现在就用上?
现在用还早了点,因为不是所有的主流浏览器都支持ES6的class。如果一定要现在就用上,就需要一个工具把class代码转换为传统的prototype代码,可以试试Babel这个工具。
需要浏览器支持ES6的class,如果遇到SyntaxError,则说明浏览器不支持class语法,请换一个最新的浏览器试试。
面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法。Javascript语言不支持"类",但是可以用一些变通的方法,模拟出"类"。
一、构造函数法
这是经典方法,也是教科书必教的方法。它用构造函数模拟"类",在其内部用this关键字指代实例对象。
function Cat() {
this.name = "张三";
}
生成实例的时候,使用new关键字。
var cat1 = new Cat();
alert(cat1.name); // 张三
类的属性和方法,还可以定义在构造函数的prototype对象之上。
Cat.prototype.makeSound = function(){
alert("你好我是类");
}
二、Object.create()法
为了解决"构造函数法"的缺点,更方便地生成对象,Javascript的国际标准ECMAScript第五版(目前通行的是第三版),提出了一个新的方法Object.create()。
用这个方法,"类"就是一个对象,不是函数。
var Cat = {
name: "李四",
makeSound: function(){ alert("早上好啊!!"); }
};
然后,直接用Object.create()生成实例,不需要用到new。
var cat1 = Object.create(Cat);
alert(cat1.name); // 张三
cat1.makeSound(); // 早上好啊!!
主流浏览器比较适配此方法。若遇到旧版本浏览器(如:IE6),可以用下面的代码自行部署。
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
这种方法比"构造函数法"简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对"类"的模拟不够全面。
三、极简主义法
3.1 封装
这种方法不使用this和prototype,代码部署起来非常简单,这大概也是它被叫做"极简主义法"的原因。
首先,它也是用一个对象模拟"类"。在这个类里面,定义一个构造函数createNew(),用来生成实例。
var Cat = {
createNew: function(){
// some code here
}
};
然后,在createNew()里面,定义一个实例对象,把这个实例对象作为返回值。
var Cat = {
createNew: function(){
var cat = {};
cat.name = "张三";
cat.makeSound = function(){ alert("沙沙沙"); };
return cat;
}
};
使用的时候,调用createNew()方法,就可以得到实例对象。
var cat1 = Cat.createNew();
cat1.makeSound(); // 沙沙沙
这种方法的好处是,容易理解,结构清晰优雅,符合传统的"面向对象编程"的构造,因此可以方便地部署下面的特性。
3.2 继承
让一个类继承另一个类,实现起来很方便。只要在前者的createNew()方法中,调用后者的createNew()方法即可。
先定义一个Animal类。
var Animal = {
createNew: function(){
var animal = {};
animal.sleep = function(){ alert("沙沙沙"); };
return animal;
}
};
然后,在Cat的createNew()方法中,调用Animal的createNew()方法。
var Cat = {
createNew: function(){
var cat = Animal.createNew();
cat.name = "张三";
cat.makeSound = function(){ alert("沙沙沙"); };
return cat;
}
};
这样得到的Cat实例,就会同时继承Cat类和Animal类。
var cat1 = Cat.createNew();
cat1.sleep(); // 沙沙沙
3.3 私有属性和私有方法
在createNew()方法中,只要不是定义在cat对象上的方法和属性,都是私有的。
var Cat = {
createNew: function(){
var cat = {};
var sound = "沙沙沙";
cat.makeSound = function(){ alert(sound); };
return cat;
}
};
上例的内部变量sound,外部无法读取,只有通过cat的公有方法makeSound()来读取。
var cat1 = Cat.createNew();
alert(cat1.sound); // undefined
3.4 数据共享
有时候,我们需要所有实例对象,能够读写同一项内部数据。这个时候,只要把这个内部数据,封装在类对象的里面、createNew()方法的外面即可。
var Cat = {
sound : "沙沙沙",
createNew: function(){
var cat = {};
cat.makeSound = function(){ alert(Cat.sound); };
cat.changeSound = function(x){ Cat.sound = x; };
return cat;
}
};
然后,生成两个实例对象:
var cat1 = Cat.createNew();
var cat2 = Cat.createNew();
cat1.makeSound(); // 沙沙沙
这时,如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响。
cat2.changeSound("啾啾啾");
cat1.makeSound(); //啾啾啾
*请认真填写需求信息,我们会在24小时内与您取得联系。