目录
[[Prototype]]机制
面向委托的设计
类理论
委托理论
比较思维模型
JavaScript创建UI控件
控件创建渲染 (ES5类继承形式)
控件“类” (类形式)
委托控件对象 (委托形式)
更简洁的设计
更好的语法
内省
[[Prototype]]
机制
[[Prototype]] 机制
: JavaScript 中这个[[Prototype]] 机制
的本质就是对象之间的关联关系。
面向委托的设计
类理论
SonA
未重写父类方法,所以.say()
执行的是父类中的say
方法
SonB
重写了父类方法,所以.say()
执行的是重写后的say
方法
class Father {
id = '';
constructor(id) {
this.id = id;
}
say() {
return this.id;
}
}
class SonA extends Father {
label = '';
constructor(id, label) {
super(id);
this.label = label;
}
}
class SonB extends Father {
label = '';
constructor(id, label) {
super(id);
this.label = label;
}
say() {
return this.label;
}
}
const sonA = new SonA('aaa', 'Lee');
const sonB = new SonB('bbb', 'Tom');
console.log(sonA, sonB); // SonA {id: 'aaa', label: 'Lee'} SonB {id: 'bbb', label: 'Tom'}
console.log(sonA.say(), sonB.say()); // aaa Tom
委托理论
同样是把实例委托给了Bar
并把Bar
委托给了Foo
,从而实现了三个对象之间的关联。但是这里更简洁,不需要那些复杂困惑的模仿类行为,比如构造函数、原型以及new
。
- 通常来说,在[[Prototype]]委托中最好把状态保存在委托者 而不是委托目标上;
- 在委托行为中,会尽量避免在[[Prototype]]链的不同级别中使用相同的命名;
- 委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象。
Foo = {
init : function(who) {
this.me = who;
},
identity : function() {
return "I am " + this.me;
}
};
Bar = Object.create(Foo); // 开始第一层委托
Bar.speak = function() { // 定义自己的功能
alert("Hello" + this.identity() + "."); // 把委托隐藏在 API 内部
}
var b1 = Object.create(Bar); // 开始第二层委托
b1.init("Saul");
var b2 = Object.create(Bar);
b2.init("Carrie");
b1.speak();
b2.speak();
互相委托(禁止): 你无法在两个或两个以上互相(双向)委托的对象之间创建循环委托。
调试: 对象关联风格的代码中,不需要关注谁“构造了”对象,浏览器调试中“构造函数名称”的跟踪没有意义。
比较思维模型
类理论
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return 'I am ' + this.me;
};
function Bar(who) {
Foo.call(this, who);
}
Bar.prototype = Object.crreate(Foo.prototype);
Bar.prototype.speak = function() {
alert('Hello, ' + this.identify() + '.');
};
var b1 = new Bar('b1');
var b2 = new Bar('b2');
b1.speak();
b2.speak();
JavaScript中的函数之所以可以访问call(..)、apply(..)和bind(..),就是因为函数本身是对象,而函数对象同样有[[Prototype]]并且关联到Function.prototype对象,因此所有函数对象都可以通过委托调用这些默认方法。
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return 'I am ' + this.me;
}
};
var Bar = Object.create(Foo);
Bar.speak = function() {
alert('Hello, ' + this.identify() + '.');
};
var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');
b1.speak();
b2.speak();
JavaScript创建UI控件
控件创建渲染 (ES5类继承形式)
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.slim.min.js"></script>
<script>
// 控件基类 宽 高 元素
function Widget(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
// 渲染方法
Widget.prototype.render = function ($where) {
// 在元素结尾插入元素
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
};
// Button控件
function Button(width, height, label) {
// 调用“super”构造函数
Widget.call(this, width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
}
// 让 Button“继承”Widget
Button.prototype = Object.create(Widget.prototype);
// 重写 render(..)
Button.prototype.render = function ($where) {
//“super”调用
Widget.prototype.render.call(this, $where);
this.$elem.click(this.onClick.bind(this));
};
// Button点击事件
Button.prototype.onClick = function (e) {
alert(`Button ${this.label} clicked!`);
};
$(document).ready(function () {
var $body = $(document.body);
var btn1 = new Button(125, 30, "Hello");
var btn2 = new Button(150, 40, "World");
btn1.render($body);
btn2.render($body);
});
控件“类” (类形式)
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.slim.min.js"></script>
<script>
// UI组件基类
class Widget {
/**
* 初始化控件
* @param {number} width 宽度
* @param {number} height 高度
* @returns {undefined}
*/
constructor(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
/**
* 渲染控件
* @param {Element} $where 父控件
* @returns {undefined}
*/
render($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
}
// Button组件
class Button extends Widget {
/**
* 初始化控件
* @param {number} width 宽度
* @param {number} height 高度
* @param {string} label 控件内容
* @returns {undefined}
*/
constructor(width, height, label) {
// 调用父类方法
super(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
}
/**
* 渲染控件 重写父类方法
* @param {Element} $where 父控件
* @returns {undefined}
*/
render($where) {
super.render($where);
this.$elem.click(this.onClick.bind(this));
}
// 点击事件
onClick(e) {
alert(`Button ${this.label} clicked!`);
}
}
// 添加UI组件
$(document).ready(function () {
var $body = $(document.body);
var btn1 = new Button(125, 30, "Hello");
var btn2 = new Button(150, 40, "World");
btn1.render($body);
btn2.render($body);
});
委托控件对象 (委托形式)
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.slim.min.js"></script>
<script>
// UI控件基类
var Widget = {
init: function (width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function ($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
};
// Button继承自Widget
var Button = Object.create(Widget);
// Button控件初始化
Button.setup = function (width, height, label) {
// 委托调用
this.init(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
};
// Button控件创建
Button.build = function ($where) {
// 委托调用
this.insert($where);
this.$elem.click(this.onClick.bind(this));
};
// Button点击事件创建
Button.onClick = function (e) {
alert(`Button ${this.label} clicked!`);
};
$(document).ready(function () {
var $body = $(document.body);
var btn1 = Object.create(Button);
btn1.setup(125, 30, "Hello");
var btn2 = Object.create(Button);
btn2.setup(150, 40, "World");
btn1.build($body);
btn2.build($body);
});
更简洁的设计
看个例子,我们有两个控制器对象,一个用来操作网页中的登录表单,另一个用来与服务器进行通信。
mock数据
{
"code": 200,
"data": {
"name": "PLee",
"age": 18
},
"msg": "操作成功"
}
在传统的类设计模式中,我们会把基础的函数定义在名为Controller的类中,然后派生两个子类LoginController和AuthController,它们都继承自Controller并且重写了一些基础行为:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>
// 基类
class Controller {
errors = null;
constructor() {
this.errors = [];
}
// 获取得到的数据
getData(title, data) {
console.log(title, data);
}
// 成功
success(res) {
this.getData("【Success】", res);
}
// 失败
failure(err) {
this.errors.push(err);
this.getData("【Error】", err);
}
}
// 登录
class LoginController extends Controller {
constructor() {
super();
}
// 获取用户名
getUser() {
return document.getElementById("login_username").value;
}
// 获取密码
getPassword() {
return document.getElementById("login_password").value;
}
// 验证 用户名/密码
validateEntry(user, pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!user) {
return this.failure("请输入用户名!");
}
if (!pw) {
return this.failure("请输入密码!");
}
// 如果执行到这里说明通过验证
return true;
}
// 重写基础的 failure()
failure(err) {
super.failure(err);
}
}
// 认证
class AuthController extends Controller {
login = null;
constructor(login) {
super();
this.login = login;
}
// 数据请求
server(url, data) {
return $.ajax({ url, data });
}
// 检查认证
checkAuth() {
var user = this.login.getUser();
var pw = this.login.getPassword();
if (this.login.validateEntry(user, pw)) {
this.server("/check-auth.json", { user, pw })
.then(res => this.success(res))
.fail(err => this.failure(err));
}
}
// 成功
success(res) {
super.success(res);
}
// 失败
failure(err) {
super.failure(err);
}
}
$(document).ready(function () {
$('#login_btn').click(function (e) {
var auth = new AuthController(new LoginController());
auth.checkAuth();
})
})
使用对象关联风格的行为委托来实现:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>
// 基础对象
var LoginController = {
errors: [],
// 获取用户名
getUser: function () {
return document.getElementById('login_username').value;
},
// 获取密码
getPassword: function () {
return document.getElementById('login_password').value;
},
// 校验用户名密码
validateEntry: function (user, pw) {
var user = user || this.getUser();
var pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure('Please enter a username & password!');
} else if (pw.length < 5) {
return this.failure('Password must be 5+ characters!');
}
return true;
},
// 相应结果
showDialog: function (title, msg) {
//给用户显示标题和消息
console.log(title, msg);
},
// 失败
failure: function (err) {
this.errors.push(err);
this.showDialog('Error', 'Login invalid:' + err);
}
}
// 登录对象
//让AuthController委托LoginController
var AuthController = Object.create(LoginController);
AuthController.errors = [];
// 登录
AuthController.checkAuth = function () {
var user = this.getUser();
var pw = this.getPassword();
if (this.validateEntry(user, pw)) {
this.server("/check-auth.json", { user, pw })
.then(res => this.accepted(res))
.fail(err => this.rejected(err));
}
}
// 发送请求
AuthController.server = function (url, data) {
return $.ajax({
url: url,
data: data
})
}
// 成功提示
AuthController.accepted = function (res) {
this.showDialog('Success', res);
}
//失败提示
AuthController.rejected = function (err) {
this.failure('Auth Failed:' + err);
}
// 登录事件
$(document).ready(function () {
$('#login_btn').click(function (e) {
AuthController.checkAuth();
})
})
更好的语法
在ES6中我们可以在任意对象的字面形式中使用简洁方法声明:
匿名函数没有name标识符,会导致:
- 1)调用栈更难追踪;
- 2) 自我引用(递归、事件(解除)绑定等)更难;
- 3)代码(稍微)更难理解。
简洁方法没有第1和第3个缺点,但是无法避免第2个缺点,如果需要自我引用,最好使用传统的具名函数表达式来定义对应的函数,不要使用简洁方法。
var LoginController = {
errors: [],
getUser(){//简洁方法
//...
},
getPassword(){//简洁方法
//...
},
//...
}
var AuthController = {
errors: [],
checkAuth(){//简洁方法
//...
},
server(url, data){//简洁方法
//...
},
//...
}
//把AuthController关联到LoginController
Object.setPrototypeOf(AuthController, LoginController);
内省
内省就是检查实例的类型。类实例的内省主要目的是通过创建方式来判断对象的结构和功能。
getPrototypeOf该方法返回指定对象的原型
isPrototypeOf返回一个布尔值,指出对象是否存在于另一个对象的原型链中。
instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上
function Foo() {
// ...
}
Foo.prototype.something = function () {
// ...
}
var a1 = new Foo();
// 之后
if (a1 instanceof Foo) {
a1.something();
}
因为Foo.prototype(不是Foo!)在a1的[[Prototype]]链上
对于“类”设计风格,如果要使用instanceof和.prototype语义来检查本例中实体的关系,那必须这样做:
function Foo() {}
function Bar() {}
Bar.prototype = Object.create(Foo.prototype);
var b1 = new Bar("b1");
// 让Foo和Bar互相关联
Bar.prototype instanceof Foo;// true
Object.getPrototypeOf(Bar.prototype) === Foo.prototype;// true
Foo.prototype.isPrototypeOf(Bar.prototype);// true
// 让b1关联到Foo和Bar
b1 instanceof Foo;// true
b1 instanceof Bar;// true
Object.getPrototypeOf(b1) === Bar.prototype;// true
Foo.prototype.isPrototypeOf(b1);// true
Bar.prototype.isPrototyoeOf(b1);// true
显然这是一种非常糟糕的方法。举例来说,(使用类时)你最直观的想法可能是使用Bar instanceof Foo(因为很容易把“实例”理解成“继承”),但是在JavaScript中这是行不通的,你必须使用Bar.prototype instanceof Foo。
instanceof 告诉我们左边要对象,右边要函数。Bar是函数,Bar.prototype是对象
使用对象关联时,所有的对象都是通过[[Prototype]]委托相互关联,下面是内省的方法:
var Foo = {/* .. */};
var Bar = Object.create(Foo);
var b1 = Object.create(Bar);
Foo.isPrototypeOf(b1);// true
Bar.isPrototypeOf(b1);// true
Object.getPrototypeOf(b1) === Bar;// true