Appearance
类
基础知识
为了和其他语言继承形态一致,JS提供了class
关键词用于模拟传统的 class
,但底层实现机制依然是原型继承。
class
只是语法糖为了让类的声明与继承更加简洁清晰。
声明定义
可以使用类声明和赋值表达式定义类,推荐使用类声明来定义类
//类声明
class User {
}
console.log(new Article());
let Article = class {
};
console.log(new User());
类方法间不需要逗号
class User {
show() {}
get() {
console.log("get method");
}
}
const bm = new User();
bm.get();
构造函数
使用 constructor
构造函数传递参数,下例中show为构造函数方法,getName为原型方法
constructor
会在 new 时自动执行
class User {
constructor(name) {
this.name = name;
this.show = function() {};
}
getName() {
return this.name;
}
}
const bm = new User("斑马兽");
console.log(bm);
构造函数用于传递对象的初始参数,但不是必须定义的,如果不设置系统会设置如下类型
- 子构造器中调用完
super
后才可以使用this
- 至于
super
的概念会在后面讲到
constructor(...args) {
super(...args);
}
原理分析
类其实是函数
class User {
}
console.log(typeof User); //function
constructor
用于定义函数代码,下面是与普通函数的对比,结构是一致的
class User {
constructor(name) {
this.name = name;
}
show() {}
}
console.dir(User);
console.log(User == User.prototype.constructor); //true
//下面是对比的普通函数
function Bm(name) {
this.name = name;
}
console.dir(Bm);
console.log(Bm == Bm.prototype.constructor); //true
在类中定义的方法也保存在函数原型中
class User {
constructor(name) {
this.name = name;
}
show() {}
}
console.dir(User);
console.log(Object.getOwnPropertyNames(User.prototype)); //["constructor", "show"]
所以下面定义的类
class User {
constructor(name) {
this.name = name;
}
show() {
console.log(this.name);
}
}
与下面使用函数的定义是一致的
function User(name) {
this.name = name;
}
Bm.prototype.show = function() {
console.log(this.name);
};
属性定义
在 class
中定义的属性为每个new
出的对象独立创建,下面定义了 site
与 name
两个对象属性
class User {
site = "斑马兽";
constructor(name) {
this.name = name;
}
show() {
console.log(this.site + ":" + this.name);
}
}
let bm = new User("斑马");
bm.show();
函数差异
class
是使用函数声明类的语法糖,但也有些区别
class
中定义的方法不能枚举
class User {
constructor(name) {
this.name = name;
}
show() {
console.log(this.name);
}
}
let bmcms = new User("斑马");
//不会枚举出show属性
for (const key in bmcms) {
console.log(key);
}
function Bm(name) {
this.name = name;
}
Bm.prototype.show = function() {
console.log(this.name);
};
let obj = new Bm("斑马兽");
for (const key in obj) {
console.log(key);
}
严格模式
class
默认使用strict
严格模式执行
class User {
constructor(name) {
this.name = name;
}
show() {
function test() {
//严格模式下输出 undefined
console.log(this);
}
test();
}
}
let bmcms = new User("斑马");
bmcms.show();
function Bm(name) {
this.name = name;
}
Bm.prototype.show = function() {
function test() {
//非严格模式输出 Window
console.log(this);
}
test();
};
let obj = new Bm("斑马兽");
obj.show();
静态访问
静态属性
静态属性即为类设置属性,而不是为生成的对象设置,下面是原理实现
function User() {}
User.site = "斑马兽";
console.dir(User);
const bm = new User();
console.log(bm.site); //undefiend
console.log(User.site); //斑马兽
在 class
中为属性添加 static
关键字即声明为静态属性
- 可以把为所有对象使用的值定义为静态属性
class Request {
static HOST = "https://www.banmashou.com";
query(api) {
return Request.HOST + "/" + api;
}
}
let request = new Request();
静态方法
指通过类访问不能使用对象访问的方法,比如系统的Math.round()
就是静态方法
- 一般来讲方法不需要对象属性参与计算就可以定义为静态方法
下面是静态方法实现原理
function User() {
this.show = function() {
return "this is a object function";
};
}
User.show = function() {
return "welcome to banmashou";
};
const bm = new User();
console.dir(bm.show()); //this is a object function
console.dir(User.show()); //welcome to banmashou
在 class
内声明的方法前使用 static
定义的方法即是静态方法
class User {
constructor(name) {
this.name = name;
}
static create(name) {
return new User(name);
}
}
const bm = User.create("斑马兽");
console.log(bm);
下面使用静态方法在课程类中的使用
const data = [
{ name: "js", price: 100 },
{ name: "mysql", price: 212 },
{ name: "vue.js", price: 98 }
];
class Lesson {
constructor(data) {
this.model = data;
}
get price() {
return this.model.price;
}
get name() {
return this.model.name;
}
//批量生成对象
static createBatch(data) {
return data.map(item => new Lesson(item));
}
//最贵的课程
static MaxPrice(collection) {
return collection.sort((a, b) => b.price() - a.price())[0];
}
}
const lessons = Lesson.createBatch(data);
console.log(lessons);
console.log(Lesson.MaxPrice(lessons).name);
访问器
使用访问器可以对对象的属性进行访问控制,下面是使用访问器对私有属性进行管理。
语法介绍
- 使用访问器可以管控属性,有效的防止属性随意修改
- 访问器就是在函数前加上
get/set
修饰,操作属性时不需要加函数的扩号,直接用函数名
class User {
constructor(name) {
this.data = { name };
}
get name() {
return this.data.name;
}
set name(value) {
if (value.trim() == "") throw new Error("invalid params");
this.data.name = value;
}
}
let bm = new User("斑马");
bm.name = "斑马兽";
console.log(bm.name);
访问控制
设置对象的私有属性有多种方式,包括后面章节介绍的模块封装。
public
public
指不受保护的属性,在类的内部与外部都可以访问到
class User {
url = "banmashou.com";
constructor(name) {
this.name = name;
}
}
let bm = new User("斑马兽");
console.log(bm.name, bm.url);
protected
protected是受保护的属性修释,不允许外部直接操作,但可以继承后在类内部访问,有以下几种方式定义
命名保护
将属性定义为以 _
开始,来告诉使用者这是一个私有属性,请不要在外部使用。
- 外部修改私有属性时可以使用访问器
setter
操作 - 但这只是提示,就像吸烟时烟盒上的吸烟有害健康,但还是可以抽的
class Article {
_host = "https://banmashou.com";
set host(url) {
if (!/^https:\/\//i.test(url)) {
throw new Error("网址错误");
}
this._host = url;
}
lists() {
return `${this._host}/article`;
}
}
let article = new Article();
console.log(article.lists()); //https://banmashou.com/article
article.host = "https://bmcms.com";
console.log(article.lists()); //https://bmcms.com/article
继承时是可以使用的
class Common {
_host = "https://banmashou.com";
set host(url) {
if (!/^https:\/\//i.test(url)) {
throw new Error("网址错误");
}
this._host = url;
}
}
class Article extends Common {
lists() {
return `${this._host}/article`;
}
}
let article = new Article();
console.log(article.lists()); //https://banmashou.com/article
article.host = "https://bmcms.com";
console.log(article.lists()); //https://bmcms.com/article
Symbol
下面使用 Symbol
定义私有访问属性,即在外部通过查看对象结构无法获取的属性
const protecteds = Symbol();
class Common {
constructor() {
this[protecteds] = {};
this[protecteds].host = "https://banmashou.com";
}
set host(url) {
if (!/^https?:/i.test(url)) {
throw new Error("非常网址");
}
this[protecteds].host = url;
}
get host() {
return this[protecteds].host;
}
}
class User extends Common {
constructor(name) {
super();
this[protecteds].name = name;
}
get name() {
return this[protecteds].name;
}
}
let bm = new User("斑马兽");
bm.host = "https://www.bmcms.com";
// console.log(bm[Symbol()]);
console.log(bm.name);
WeakMap
WeakMap 是一组键/值对的集,下面利用WeakMap
类型特性定义私有属性
const _host = new WeakMap();
class Common {
constructor() {
_host.set(this, "https://banmashou.com");
}
set host(url) {
if (!/^https:\/\//i.test(url)) {
throw new Error("网址错误");
}
_host.set(this, url);
}
}
class Article extends Common {
constructor() {
super();
}
lists() {
return `${_host.get(this)}/article`;
}
}
let article = new Article();
console.log(article.lists()); //https://banmashou.com/article
article.host = "https://bmcms.com";
console.log(article.lists()); //https://bmcms.com/article
也可以统一定义私有属性
const protecteds = new WeakMap();
class Common {
constructor() {
protecteds.set(this, {
host: "https://banmashou.com",
port: "80"
});
}
set host(url) {
if (!/^https:\/\//i.test(url)) {
throw new Error("网址错误");
}
protecteds.set(this, { ...protecteds.get(this), host: url });
}
}
class Article extends Common {
constructor() {
super();
}
lists() {
return `${protecteds.get(this).host}/article`;
}
}
let article = new Article();
console.log(article.lists()); //https://banmashou.com/article
article.host = "https://bmcms.com";
console.log(article.lists()); //https://bmcms.com/article
private
private
指私有属性,只在当前类可以访问到,并且不允许继承使用
- 为属性或方法名前加
#
为声明为私有属性 - 私有属性只能在声明的类中使用
下面声明私有属性 #host
与私有方法 check
用于检测用户名
class User {
//private
#host = "https://banmashou.com";
constructor(name) {
this.name = name ;
this.#check(name);
}
set host(url) {
if (!/^https?:/i.test(url)) {
throw new Error("非常网址");
}
this.#host = url;
}
get host() {
return this.#host;
}
#check = () => {
if (this.name.length <= 5) {
throw new Error("用户名长度不能小于五位");
}
return true;
};
}
let bm = new User("斑马兽");
bm.host = "https://www.bmcms.com";
console.log(bm.host);
属性保护
保护属性并使用访问器控制
const protecteds = Symbol("protected");
class User {
constructor(name) {
this[protecteds] = { name };
}
get name() {
return this[protecteds].name;
}
set name(value) {
if (value.trim() == "") throw new Error("invalid params");
this[protecteds].name = value;
}
}
let bm = new User("斑马");
bm.name = "斑马兽";
console.log(bm.name);
console.log(Object.keys(bm));
详解继承
属性继承
属性继承的原型如下
function User(name) {
this.name = name;
}
function Admin(name) {
User.call(this, name);
}
let bm = new Admin("斑马兽");
console.log(bm);
这就解释了为什么在子类构造函数中要先执行super
class User {
constructor(name) {
this.name = name;
}
}
class Admin extends User {
constructor(name) {
super(name);
}
}
let bm = new Admin("斑马兽");
console.log(bm);
继承原理
class
继承内部使用原型继承
class User {
show() {
console.log("user.show");
}
}
class Admin extends User {
info() {
this.show();
}
}
let bm = new Admin();
console.dir(bm);
方法继承
原生的继承主要是操作原型链,实现起来比较麻烦,使用 class
就要简单的多了。
- 继承时必须在子类构造函数中调用 super() 执行父类构造函数
- super.show() 执行父类方法
下面是子类继承了父类的方法show
class Person {
constructor(name) {
this.name = name;
}
show() {
return `斑马兽: ${this.name}`;
}
}
class User extends Person {
constructor(name) {
super(name);
}
run() {
return super.show();
}
}
const bm = new User("斑马");
console.dir(bm.run());
可以使用 extends
继承表达式返回的类
function controller() {
return class {
show() {
console.log("user.show");
}
};
}
class Admin extends controller() {
info() {
this.show();
}
}
let bm = new Admin();
console.dir(bm);
super
表示从当前原型中执行方法,
- super 一直指向当前对象
下面是使用 this
模拟super
,会有以下问题
- 但
this
指向当前对象,结果并不是admin
的name
值
let user = {
name: "user",
show() {
return this.name;
}
};
let admin = {
__proto__: user,
name: "admin",
show() {
return this.__proto__.show();
}
};
console.log(admin.show());
为了解决以上问题,需要调用父类方法时传递this
let user = {
name: "user",
show() {
return this.name;
}
};
let admin = {
__proto__: user,
name: "admin",
show() {
return this.__proto__.show.call(this);
}
};
console.log(admin.show());
上面看似结果正常,但如果是多层继承时,会出现新的问题
- 因为始终传递的是当前对象
this
,造成从this
原型循环调用
let common = {
show() {
console.log("common.init");
}
};
let user = {
__proto__: common,
name: "user",
show() {
return this.__proto__.show.call(this);
}
};
let admin = {
__proto__: user,
name: "admin",
get() {
return this.__proto__.show.call(this);
}
};
console.log(admin.get());
为了解决以上问题 js
提供了 super
关键字
- 使用
super
调用时,在所有继承中this
始终为调用对象 super
是用来查找当前对象的原型,而不像上面使用this
查找原型造成死循环- 也就是说把查询原型方法的事情交给了
super
,this
只是单纯的调用对象在各个继承中使用
let common = {
show() {
return this.name;
}
};
let user = {
__proto__: common,
name: "user",
show() {
return super.show(this);
}
};
let admin = {
__proto__: user,
name: "admin",
get() {
return super.show();
}
};
console.log(admin.get());
super
只能在类或对象的方法中使用,而不能在函数中使用,下面将产生错误
let user = {
name: "user",
show() {
return this.name;
}
};
let admin = {
__proto__: user,
name: "admin",
get: function() {
return super.show();
}
};
console.log(admin.get()); //Uncaught SyntaxError: 'super' keyword unexpected here
constructor
super
指调父类引用,在构造函数constructor
中必须先调用super()
super()
指调用父类的构造函数- 必须在
constructor
函数里的this
调用前执行super()
class User {
constructor(name) {
this.name = name;
}
show() {
console.log(this.name);
}
}
class Admin extends User {
constructor(name) {
super(name);
}
}
let bm = new Admin("斑马兽");
bm.show();
constructor
中先调用 super
方法的原理如下
function Parent(name) {
this.name = name;
}
function User(...args) {
Parent.apply(this, args);
}
User.prototype = Object.create(User.prototype)
User.prototype.constructor = User;
const bm = new User("斑马兽");
console.log(bm.name);
父类方法
使用super
可以执行父类方法
- 不添加方法名是执调用父类构造函数
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Admin extends User {
constructor(name) {
super(name);
}
}
const bm = new Admin("斑马兽");
console.log(bm.getName());
下面是通过父类方法获取课程总价
class Controller {
sum() {
return this.data.reduce((t, c) => t + c.price, 0);
}
}
class Lesson extends Controller {
constructor(lessons) {
super();
this.data = lessons;
}
info() {
return {
totalPrice: super.sum(),
data: this.data
};
}
}
let data = [
{ name: "js", price: 100 },
{ name: "mysql", price: 212 },
{ name: "vue.js", price: 98 }
];
const bm = new Lesson(data);
console.log(bm.info());
方法覆盖
子类存在父类同名方法时使用子类方法
class User {
constructor(name) {
this.name = name;
}
say() {
return this.name;
}
}
class Admin extends User {
constructor(name) {
super(name);
}
say() {
return "斑马兽:" + super.say();
}
}
const bm = new Admin("斑马");
console.log(bm.say());
下面是覆盖父类方法,只获取课程名称
class Controller {
say() {
return this.name;
}
total() {
return this.data.reduce((t, c) => t + c.price, 0);
}
getByKey(key) {
return this.data.filter(item => item.name.includes(key));
}
}
class Lesson extends Controller {
constructor(lessons) {
super();
this.data = lessons;
}
getByKey(key) {
return super.getByKey(key).map(item => item.name);
}
}
let data = [
{ name: "js", price: 100 },
{ name: "mysql", price: 212 },
{ name: "vue.js", price: 98 }
];
const bm = new Lesson(data);
console.log(bm.getByKey("js"));
静态继承
静态的属性和方法也是可以被继承使用的,下面是原理分析
function User() {}
User.site = "斑马兽";
User.url = function() {
return "banmashou.com";
};
function Admin() {}
Admin.__proto__ = User;
console.dir(Admin);
console.log(Admin.url());
下面使用 class
来演示静态继承
class User {
static site = "斑马兽";
static host() {
return "banmashou.com";
}
}
class Admin extends User {}
console.dir(Admin);
对象检测
instanceof
使用 instanceof
用于检测,下面是在原型中的分析(已经在原型与继承中讲过)
function User() {}
function Admin() {}
Admin.prototype = Object.create(User.prototype);
let bm = new Admin();
console.log(bm instanceof Admin); //true
console.log(bm instanceof User); //true
console.log(bm.__proto__ == Admin.prototype);
console.log(bm.__proto__.__proto__ == User.prototype);
下面是递归检测原型的代码,帮助你分析 instanceof
的原理
function checkPrototype(obj, constructor) {
if (!obj.__proto__) return false;
if (obj.__proto__ == constructor.prototype) return true;
return checkPrototype(obj.__proto__, constructor);
}
class
内部实现就是基于原型,所以使用instanceof
判断和上面原型是一样的
class User {}
class Admin extends User {}
let bm = new Admin();
console.log(bm instanceof Admin);
console.log(bm instanceof User);
isPrototypeOf
使用 isPrototypeOf
判断一个对象是否在另一个对象的原型链中,下面是原理分析
const a = {};
const b = {
__proto__: a
};
const c = {
__proto__: b
};
console.log(a.isPrototypeOf(b)); //true
console.log(a.isPrototypeOf(c)); //true
下面在使用 class 语法中使用
class User {}
class Admin extends User {}
let bm = new Admin();
console.log(Admin.prototype.isPrototypeOf(bm));
console.log(User.prototype.isPrototypeOf(bm));
继承内置类
使用原型扩展内置类
function Arr(...args) {
args.forEach(item => this.push(item));
this.first = function() {
return this[0];
};
this.max = function() {
return this.data.sort((a, b) => b - a)[0];
};
}
let a = [1, 23];
Arr.prototype = Object.create(Array.prototype);
let arr = new Arr("斑马兽", 2, 3);
console.log(arr.first());
使用 class
扩展内置类
class NewArr extends Array {
constructor(...args) {
super(...args);
}
first() {
return this[0];
}
add(value) {
this.push(value);
}
remove(value) {
let pos = this.findIndex(curValue => {
return curValue == value;
});
this.splice(pos, 1);
}
}
let bm = new NewArr(5, 3, 2, 1);
console.log(bm.length); //4
console.log(bm.first()); //5
bm.add("banmashou");
console.log(bm.join(",")); //5,3,2,1,banmashou
bm.remove("3");
console.log(bm.join(",")); //5,2,1,banmashou
mixin
关于mixin
的使用在原型章节已经讨论过,在class
使用也是相同的原理
JS
不能实现多继承,如果要使用多个类的方法时可以使用mixin
混合模式来完成。
mixin
类是一个包含许多供其它类使用的方法的类mixin
类不用来继承做为其它类的父类
其他语言也有类似的操作比如
php
语言中可以使用trait
完成类似操作
const Tool = {
max(key) {
return this.data.sort((a, b) => b[key] - a[key])[0];
}
};
class Lesson {
constructor(lessons) {
this.lessons = lessons;
}
get data() {
return this.lessons;
}
}
Object.assign(Lesson.prototype, Tool);
const data = [
{ name: "js", price: 100 },
{ name: "mysql", price: 212 },
{ name: "vue.js", price: 98 }
];
let bm = new Lesson(data);
console.log(bm.max("price"));
实例操作
<style>
* {
padding: 0;
margin: 0;
box-sizing: content-box;
}
body {
padding: 30px;
}
.slide {
width: 300px;
display: flex;
flex-direction: column;
/* box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); */
}
.slide dt {
height: 30px;
background: #34495e;
color: white;
display: flex;
align-items: center;
padding-left: 10px;
cursor: pointer;
}
.slide dt:first-of-type {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.slide dd {
height: 100px;
background: #f1c40f;
overflow: hidden;
}
.slide dd div {
padding: 10px;
}
.slide dd:last-of-type {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
</style>
<body>
<div class="slide s1">
<dt>斑马兽</dt>
<dd>
<div>banmashou.com</div>
</dd>
<dt>斑马兽</dt>
<dd>
<div>bmcms.com</div>
</dd>
<dt>斑马兽</dt>
<dd>
<div>bmcms.com</div>
</dd>
</div>
</body>
<script>
class Animation {
constructor(el) {
this.el = el;
this.timeout = 5;
this.isShow = true;
this.defaultHeight = this.height;
}
hide(callback) {
this.isShow = false;
let id = setInterval(() => {
if (this.height <= 0) {
clearInterval(id);
callback && callback();
return;
}
this.height = this.height - 1;
}, this.timeout);
}
show(callback) {
this.isShow = false;
let id = setInterval(() => {
if (this.height >= this.defaultHeight) {
clearInterval(id);
callback && callback();
return;
}
this.height = this.height + 1;
}, this.timeout);
}
get height() {
return window.getComputedStyle(this.el).height.slice(0, -2) * 1;
}
set height(height) {
this.el.style.height = height + "px";
}
}
class Slide {
constructor(el) {
this.el = document.querySelector(el);
this.links = this.el.querySelectorAll("dt");
this.panels = [...this.el.querySelectorAll("dd")].map(
item => new Panel(item)
);
this.bind();
}
bind() {
this.links.forEach((item, i) => {
item.addEventListener("click", () => {
this.action(i);
});
});
}
action(i) {
Panel.hideAll(Panel.filter(this.panels, i), () => {
this.panels[i].show();
});
}
}
class Panel extends Animation {
static num = 0;
static hideAll(items, callback) {
if (Panel.num > 0) return;
items.forEach(item => {
Panel.num++;
item.hide(() => {
Panel.num--;
});
});
callback && callback();
}
static filter(items, i) {
return items.filter((item, index) => index != i);
}
}
let bm = new Slide(".s1");
</script>