私有的和受保护的属性和方法
概述
- 面向对象编程最重要的原则之一 —— 将内部接口与外部接口分隔开来
- 在
JavaScript
中,有两种类型的对象字段(属性和方法):- 公共的:可从任何地方访问。它们构成了外部接口。到目前为止,我们只使用了公共的属性和方法
- 私有的:只能从类的内部访问。这些用于内部接口
- 在许多其他编程语言中,还存在“受保护”的字段:
只能从类的内部和基于其扩展的类的内部访问(例如私有的,但可以从继承的类进行访问)
受保护的字段不是在语言级别的JavaScript
中实现的,但实际上它们非常方便,因为它们是在JavaScript
中模拟的类定义语法
内部接口和外部接口
- 内部接口 —— 可以通过该类的其他方法访问,但不能从外部访问的方法和属性
- 外部接口 —— 也可以从类的外部访问的方法和属性
受保护的
- 首先,让我们做一个简单的咖啡机类:
- 现在,属性
waterAmount
和power
是公共的。我们可以轻松地从外部将它们get/set
成任何值
- 现在,属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CoffeeMachine { waterAmount = 0; // 内部的水量 constructor(power) { this.power = power; alert( `Created a coffee-machine, power: ${power}` ); } } // 创建咖啡机 let coffeeMachine = new CoffeeMachine(100); // 加水 coffeeMachine.waterAmount = 200; |
- 让我们将
waterAmount
属性更改为受保护的属性,以对其进行更多控制- 例如,我们不希望任何人将它的值设置为小于零的数
- 受保护的属性通常以下划线
_
作为前缀 - 这不是在语言级别强制实施的,但是程序员之间有一个众所周知的约定,即不应该从外部访问此类型的属性和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class CoffeeMachine { _waterAmount = 0; set waterAmount(value) { if (value < 0) { value = 0; } this._waterAmount = value; } get waterAmount() { return this._waterAmount; } constructor(power) { this._power = power; } } // 创建咖啡机 let coffeeMachine = new CoffeeMachine(100); // 加水 coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10 |
只读的
- 对于
power
属性,让我们将它设为只读- 有时候一个属性必须只能被在创建时进行设置,之后不再被修改
- 咖啡机就是这种情况:功率永远不会改变
- 要做到这一点,我们只需要设置 getter,而不设置 setter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class CoffeeMachine { // ... constructor(power) { this._power = power; } get power() { return this._power; } } // 创建咖啡机 let coffeeMachine = new CoffeeMachine(100); alert(`Power is: ${coffeeMachine.power}W`); // 功率是:100W coffeeMachine.power = 25; // Error(没有 setter) |
getter/setter
函数
- 这里我们使用了
getter/setter
语法 - 但大多数时候首选
get.../set...
函数,像这样:- 这看起来有点长,但函数更灵活。它们可以接受多个参数(即使我们现在还不需要)
- 另一方面,
get/set
语法更短,所以最终没有严格的规定,而是由你自己来决定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CoffeeMachine { _waterAmount = 0; setWaterAmount(value) { if (value < 0) value = 0; this._waterAmount = value; } getWaterAmount() { return this._waterAmount; } } new CoffeeMachine().setWaterAmount(100); |
注意1
- 受保护的字段是可以被继承的
- 如果我们继承
class MegaMachine extends CoffeeMachine
,那么什么都无法阻止我们从新的类中的方法访问this._waterAmount
或this._power
- 所以受保护的字段是自然可被继承的
- 如果我们继承
私有的
- 这是一个最近添加到
JavaScript
的特性 - 私有属性和方法应该以
#
开头。它们只在类的内部可被访问- 例如,这儿有一个私有属性
#waterLimit
和检查水量的私有方法#fixWaterAmount
:
- 例如,这儿有一个私有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CoffeeMachine { #waterLimit = 200; #fixWaterAmount(value) { if (value < 0) return 0; if (value > this.#waterLimit) return this.#waterLimit; } setWaterAmount(value) { this.#waterLimit = this.#fixWaterAmount(value); } } let coffeeMachine = new CoffeeMachine(); // 不能从类的外部访问类的私有属性和方法 coffeeMachine.#fixWaterAmount(123); // Error coffeeMachine.#waterLimit = 1000; // Error |
- 私有字段与公共字段不会发生冲突。我们可以同时拥有私有的
#waterAmount
和公共的waterAmount
字段- 例如,让我们使
waterAmount
成为#waterAmount
的一个访问器:
- 例如,让我们使
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CoffeeMachine { #waterAmount = 0; get waterAmount() { return this.#waterAmount; } set waterAmount(value) { if (value < 0) value = 0; this.#waterAmount = value; } } let machine = new CoffeeMachine(); machine.waterAmount = 100; alert(machine.#waterAmount); // Error |
- 与受保护的字段不同,私有字段由语言本身强制执行
- 但是如果我们继承自
CoffeeMachine
,那么我们将无法直接访问#waterAmount
。我们需要依靠waterAmount
getter/setter
: - 在许多情况下,这种限制太严重了
如果我们扩展CoffeeMachine
,则可能有正当理由访问其内部
这就是为什么大多数时候都会使用受保护字段,即使它们不受语言语法的支持
- 但是如果我们继承自
1 2 3 4 5 |
class MegaCoffeeMachine extends CoffeeMachine { method() { alert( this.#waterAmount ); // Error: can only access from CoffeeMachine } } |
注意2
- 私有字段不能通过
this[name]
访问- 私有字段很特别
- 正如我们所知道的,通常我们可以使用
this[name]
访问字段: - 对于私有字段来说,这是不可能的:
this['#name']
不起作用。这是确保私有性的语法限制
1 2 3 4 5 6 7 |
class User { ... sayHi() { let fieldName = "name"; alert(`Hello, ${this[fieldName]}`); } } |
扩展内建类
概述
- 内建的类,例如
Array
,Map
等也都是可以扩展的(extendable
) - 例如,这里有一个继承自原生
Array
的类PowerArray
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 给 PowerArray 新增了一个方法(可以增加更多) class PowerArray extends Array { isEmpty() { return this.length === 0; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); // false |
- 请注意一个非常有趣的事儿
- 内建的方法例如
filter
,map
等 —— 返回的正是子类PowerArray
的新对象 - 它们内部使用了对象的
constructor
属性来实现这一功能
- 内建的方法例如
1 |
arr.constructor === PowerArray |
- 当
arr.filter()
被调用时,它的内部使用的是arr.constructor
来创建新的结果数组,而不是使用原生的Array
- 我们可以在结果数组上继续使用
PowerArray
的方法 - 甚至,我们可以定制这种行为
- 我们可以给这个类添加一个特殊的静态
getter
Symbol.species
,它会返回JavaScript
在内部用来在map
和filter
等方法中创建新实体的constructor
- 如果我们希望像
map
或filter
这样的内建方法返回常规数组,我们可以在Symbol.species
中返回Array
,就像这样:
- 我们可以在结果数组上继续使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class PowerArray extends Array { isEmpty() { return this.length === 0; } // 内建方法将使用这个作为 constructor static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组 let filteredArr = arr.filter(item => item >= 10); // filteredArr 不是 PowerArray,而是 Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function |
- 其他集合的工作方式类似
- 其他集合,例如
Map
和Set
的工作方式类似。它们也使用Symbol.species
- 其他集合,例如
内建类没有静态方法继承
- 内建对象有它们自己的静态方法,例如
Object.keys
,Array.isArray
等- 如我们所知道的,原生的类互相扩展。例如,
Array
扩展自Object
- 通常,当一个类扩展另一个类时,静态方法和非静态方法都会被继承
- 但内建类却是一个例外。它们相互间不继承静态方法
- 如我们所知道的,原生的类互相扩展。例如,
- 例如,
Array
和Date
都继承自Object
,所以它们的实例都有来自Object.prototype
的方法- 但
Array.[[Prototype]]
并不指向Object
,所以它们没有例如Array.keys()
(或Date.keys()
)这些静态方法
- 但
类检查:instanceof
概述
instanceof
操作符用于检查一个对象是否属于某个特定的class
- 同时,它还考虑了继承
- 在许多情况下,可能都需要进行此类检查
- 例如,它可以被用来构建一个 多态性(
polymorphic
) 的函数,该函数根据参数的类型对参数进行不同的处理
- 例如,它可以被用来构建一个 多态性(
instanceof
操作符
- 语法
- 如果
obj
隶属于Class
类(或Class
类的衍生类),则返回true
。
- 如果
1 |
obj instanceof Class |
- 例如:
1 2 3 4 5 |
class Rabbit {} let rabbit = new Rabbit(); // rabbit 是 Rabbit class 的对象吗? alert( rabbit instanceof Rabbit ); // true |
- 它还可以与构造函数一起使用:
1 2 3 4 |
// 这里是构造函数,而不是 class function Rabbit() {} alert( new Rabbit() instanceof Rabbit ); // true |
- 与诸如
Array
之类的内建class
一起使用:- 有一点需要留意,
arr
同时还隶属于Object
类。因为从原型上来讲,Array
是继承自Object
的 - 通常,
instanceof
在检查中会将原型链考虑在内
- 有一点需要留意,
1 2 3 |
let arr = [1, 2, 3]; alert( arr instanceof Array ); // true alert( arr instanceof Object ); // true |
- 此外,我们还可以在静态方法
Symbol.hasInstance
中设置自定义逻辑
obj instanceof Class
的执行过程大致如下:
- 如果这儿有静态方法
Symbol.hasInstance
,那就直接调用这个方法:
1 2 3 4 5 6 7 8 9 10 11 |
// 设置 instanceOf 检查 // 并假设具有 canEat 属性的都是 animal class Animal { static [Symbol.hasInstance](obj) { if (obj.canEat) return true; } } let obj = { canEat: true }; alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用 |
- 大多数 class 没有
Symbol.hasInstance
。在这种情况下,标准的逻辑是:使用obj instanceOf Class
检查Class.prototype
是否等于obj
的原型链中的原型之一- 换句话说就是,一个接一个地比较:
1 2 3 4 5 6 7 |
obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // 如果任意一个的答案为 true,则返回 true // 否则,如果我们已经检查到了原型链的尾端,则返回 false |
- 上面那个例子中,
rabbit.__proto__ === Rabbit.prototype
,所以立即就给出了结果- 在继承的例子中,匹配将在第二步进行:
1 2 3 4 5 6 7 8 |
class Animal {} class Rabbit extends Animal {} let rabbit = new Rabbit(); alert(rabbit instanceof Animal); // true // rabbit.__proto__ === Animal.prototype(无匹配) // rabbit.__proto__.__proto__ === Animal.prototype(匹配!) |
objA.isPrototypeOf(objB)
- 这里还要提到一个方法
objA.isPrototypeOf(objB)
- 如果
objA
处在objB
的原型链中,则返回true
- 所以,可以将
obj instanceof Class
检查改为Class.prototype.isPrototypeOf(obj)
- 如果
1 2 3 4 5 6 7 8 |
function Rabbit() {} let rabbit = new Rabbit(); // 修改了 prototype Rabbit.prototype = {}; // ...再也不是 rabbit 了! alert( rabbit instanceof Rabbit ); // false |
使用 Object.prototype.toString
来揭示类型
- 一个普通对象被转化为字符串时为
[object Object]
:- 这是通过
toString
方法实现的
- 这是通过
1 2 3 4 |
let obj = {}; alert(obj); // [object Object] alert(obj.toString()); // 同上 |
- 这儿有一个隐藏的功能,该功能可以使
toString
实际上比这更强大- 我们可以将其作为
typeof
的增强版或者instanceof
的替代方法来使用 - 按照 规范所讲,内建的
toString
方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值 - 对于
number
类型,结果是[object Number]
- 对于
boolean
类型,结果是[object Boolean]
- 对于
null
:[object Null]
- 对于
undefined
:[object Undefined]
- 对于数组:
[object Array]
- 等(可自定义)
- 我们可以将其作为
1 2 3 4 5 6 7 |
// 方便起见,将 toString 方法复制到一个变量中 let objectToString = Object.prototype.toString; // 它是什么类型的? let arr = []; alert( objectToString.call(arr) ); // [object Array] |
- 这里我们用到了
call
方法来在上下文this=arr
中执行函数objectToString
- 在内部,
toString
的算法会检查this
,并返回相应的结果
- 在内部,
1 2 3 4 5 |
let s = Object.prototype.toString; alert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call(alert) ); // [object Function] |
Symbol.toStringTag
- 可以使用特殊的对象属性
Symbol.toStringTag
自定义对象的toString
方法的行为
1 2 3 4 5 |
let user = { [Symbol.toStringTag]: "User" }; alert( {}.toString.call(user) ); // [object User] |
- 对于大多数特定于环境的对象,都有一个这样的属性
- 下面是一些特定于浏览器的示例:
1 2 3 4 5 6 |
// 特定于环境的对象和类的 toStringTag: alert( window[Symbol.toStringTag]); // Window alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest alert( {}.toString.call(window) ); // [object Window] alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest] |
Mixin
模式
概述
- 在
JavaScript
中,我们只能继承单个对象- 每个对象只能有一个
[[Prototype]]
。并且每个类只可以扩展另外一个类
- 每个对象只能有一个
- 但是有些时候这种设定(译注:单继承)会让人感到受限制
- 例如,我有一个
StreetSweeper
类和一个Bicycle
类,现在想要一个它们的混合体:StreetSweepingBicycle
类 - 或者,我们有一个
User
类和一个EventEmitter
类来实现事件生成(event generation
),并且我们想将EventEmitter
的功能添加到User
中,以便我们的用户可以触发事件(emit event
)
- 例如,我有一个
- 有一个概念可以帮助我们,叫做 “
mixin
”- 是一个类,其方法可被其他类使用,而无需继承
- 换句话说,
mixin
提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中
Mixin
示例
- 在
JavaScript
中构造一个mixin
最简单的方式就是构造一个拥有实用方法的对象,以便我们可以轻松地将这些实用的方法合并到任何类的原型中 - 例如,这个名为
sayHiMixin
的mixin
用于给User
添加一些“语言功能”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// mixin let sayHiMixin = { sayHi() { alert(`Hello ${this.name}`); }, sayBye() { alert(`Bye ${this.name}`); } }; // 用法: class User { constructor(name) { this.name = name; } } // 拷贝方法 Object.assign(User.prototype, sayHiMixin); // 现在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude! |
- 这里没有继承,只有一个简单的方法拷贝
- 因此,我们可以让
User
在继承另一个类的同时,使用mixin
来 “mix-in
”(混合)其它方法,就像这样:
- 因此,我们可以让
1 2 3 4 5 |
class User extends Person { // ... } Object.assign(User.prototype, sayHiMixin); |
Mixin
可以在自己内部使用继承- 例如,这里的
sayHiMixin
继承自sayMixin
: - 注意,在
sayHiMixin
内部对父类方法super.say()
的调用(在标有(*)
的行)会在mixin
的原型中查找方法,而不是在class
中查找 - 这是因为方法
sayHi
和sayBye
最初是在sayHiMixin
中创建的。因此,即使复制了它们,但是它们的[[HomeObject]]
内部属性仍引用的是sayHiMixin
- 当
super
在[[HomeObject]].[[Prototype]]
中寻找父方法时,意味着它搜索的是sayHiMixin.[[Prototype]]
,而不是User.[[Prototype]]
- 例如,这里的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
let sayMixin = { say(phrase) { alert(phrase); } }; let sayHiMixin = { __proto__: sayMixin, // (或者,我们可以在这儿使用 Object.setPrototypeOf 来设置原型) sayHi() { // 调用父类方法 super.say(`Hello ${this.name}`); // (*) }, sayBye() { super.say(`Bye ${this.name}`); // (*) } }; class User { constructor(name) { this.name = name; } } // 拷贝方法 Object.assign(User.prototype, sayHiMixin); // 现在 User 可以打招呼了 new User("Dude").sayHi(); // Hello Dude! |
EventMixin
- 现在让我们为实际运用构造一个
mixin
- 例如,许多浏览器对象的一个重要功能是它们可以生成事件
- 事件是向任何有需要的人“广播信息”的好方法
- 因此,让我们构造一个
mixin
,使我们能够轻松地将与事件相关的函数添加到任意class/object
中 Mixin
将提供.trigger(name, [...data])
方法,以在发生重要的事情时“生成一个事件”
name
参数(arguments
)是事件的名称,[...data]
是可选的带有事件数据的其他参数(arguments
)- 此外还有
.on(name, handler)
方法,它为具有给定名称的事件添加了handler
函数作为监听器(listener
)
当具有给定name
的事件触发时将调用该方法,并从.trigger
调用中获取参数(arguments
) - 还有
.off(name, handler)
方法,它会删除handler
监听器(listener
) - 添加完
mixin
后,对象user
将能够在访客登录时生成事件"login"
- 另一个对象,例如
calendar
可能希望监听此类事件以便为登录的人加载日历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
let eventMixin = { /** * 订阅事件,用法: * menu.on('select', function(item) { ... } */ on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } this._eventHandlers[eventName].push(handler); }, /** * 取消订阅,用法: * menu.off('select', handler) */ off(eventName, handler) { let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { handlers.splice(i--, 1); } } }, /** * 生成具有给定名称和数据的事件 * this.trigger('select', data1, data2); */ trigger(eventName, ...args) { if (!this._eventHandlers?.[eventName]) { return; // 该事件名称没有对应的事件处理程序(handler) } // 调用事件处理程序(handler) this._eventHandlers[eventName].forEach(handler => handler.apply(this, args)); } }; |
- 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 创建一个 class class Menu { choose(value) { this.trigger("select", value); } } // 添加带有事件相关方法的 mixin Object.assign(Menu.prototype, eventMixin); let menu = new Menu(); // 添加一个事件处理程序(handler),在被选择时被调用: menu.on("select", value => alert(`Value selected: ${value}`)); // 触发事件 => 运行上述的事件处理程序(handler)并显示: // 被选中的值:123 menu.choose("123"); |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 【Javascript】原型继承,F.prototype,原生的原型,原型方法,“Very plain”对象04/12
- ♥ WebSocket协议相关学习一03/24
- ♥ 【Javascript】装饰器,转发,call,apply,函数绑定,箭头函数04/06
- ♥ 【Javascript】try...catch,try…catch…finally,自定义Error,扩展Error04/13
- ♥ 【Javascript】全局对象,函数对象,new_function,setTimeout,setInterval04/06
- ♥ 【Javascript】属性标志,属性描述符,getter,setter04/12