持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
原型模式定义
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
传统的原型模式就是克隆,但这在 JS 中并不常用,JS 中的继承是利用原型链来实现克隆的功能,并不是真的去克隆。
class CloneDemo {
name: string = 'clone demo'
clone(): CloneDemo {
return new CloneDemo()
}
}
最符合原型模式的应用场景就是 Object.create
,它可以指定原型。
const obj1 = {}
obj1.__proto__ // 指向Object.prototype
const obj2 = Object.create({x: 100})
obj2.__proto__ // 指向{x: 100}
原型与原型链
记住一下三句话:
- 函数或者 class 都有显示原型
prototype
- 对象都有显示原型
__proto__
- 对象
__proto__
指向其构造函数的prototype
函数和显示原型 prototype
js 内置了很多构造函数,比如 Object, Array 等,所以它们也有显示原型prototype
。
自定义的函数也有显示原型 prototype
// @ts-nocheck
function Foo(name: string, age: number) {
this.name = name
this.age = age
this.getName = function () {
return this.name
}
}
Foo.prototype.sayHi = function () {
alert('hi')
}
const foo = new Foo('xiaoming', 20)
console.log(foo)
name
,age
,getName
是对象本身的属性和方法,sayHi
是原型上的方法。这里只是为了区分什么是对象本身的属性和方法,什么是原型上的属性和方法,在工作中属性写在对象本身上,方法一般写在原型上。
对象和隐式原型 __proto__
引用类型
JS 所有的引用类型对象都是通过函数创建的,都有 __proto__
,指向其构造函数的 prototype
。
const obj = {} // 相当于 new Object()
obj.__proto__ === Object.prototype
const arr = [] // 相当于 new Array()
arr.__proto__ === Array.prototype
const f1 = new Foo('张三', 20)
f1.__proto__ === Foo.prototype
const f2 = new Foo('李四', 21)
f2.__proto__ === Foo.prototype
访问对象属性或 API 时,首先查找自身属性(即对象本身的属性),然后查找它的 __proto__
的属性。
f1.name // name 是对象本身的属性
f1.getName() // getName 是原型对象上的方法
值类型
值类型没有 __proto__
,但它依然可访问 API 。因为 JS 会先将它包装为引用类型,然后触发 API。
const str = 'abc'
str.slice(0, 1) // 调用 String.prototype.string
原型链
一个对象的 __proto__
指向它构造函数的 prototype
,而 prototype
本身也是一个对象,也会指向它构造函数的 prototype
,于是就形成了原型链。
关于js中的原型,可以参考这篇文章深刻理解js中的原型。
class 是函数的语法糖
class Foo {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
getName() {
return this.name
}
sayHi() {
alert('hi')
}
}
通过babel或者ts编译后:
var Foo = (function () {
function Foo(name, age) {
this.name = name;
this.age = age;
}
Foo.prototype.getName = function () {
return this.name;
};
Foo.prototype.sayHi = function () {
alert('hi');
};
return Foo;
}());
继承
class People {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
eat() {
alert(`${this.name} eat something`)
}
speak() {
alert(`My name is ${this.name}, age ${this.age}`)
}
}
class Student extends People {
school: string
constructor(name: string, age: number, school: string) {
super(name, age)
this.school = school
}
study() {
alert(`${this.name} study`)
}
}
const s1 = new Student('aa', 20, 'xx')
s1.study()
s1.eat()
我们来看看继承的原型链:
extends
的继承语法等价于如下代码:
Student.prototype = new People()
Student的显示原型prototype
等于People的实例,这个实例的__proto__
又指向People.prototype
,所以Student.prototype
就指向了 People.prototype
,这样就形成了原型链。
JS 对象属性描述符
属性描述符: 用于描述对象属性的一些特性。
属性描述符设置和获取
获取属性描述符
const obj = { x: 100 }
Object.getOwnPropertyDescriptor(obj, 'x')
设置属性描述符
Object.defineProperty(obj, 'y', {
value: 200,
writable: false,
// 其他...
// PS: 还可以定义 get set
})
使用 Object.defineProperty
定义新属性,属性描述符会默认为 false { configurable: false, enumerable: false, writable: false }
。
而用 { x: 100 }
字面量形式定义属性,属性描述符默认为 true。
解释各个描述符
value
属性值:值类型、引用类型、函数等
const obj = { x: 100 }
Object.defineProperty(obj, 'x', {
value: 101,
})
如果不设置 value,而是通过 get set 函数来设置 ,则打印 obj 就看不到属性。
const obj = {}
let x = 100
Object.defineProperty(obj, 'x', {
get() {
return x
},
set(newValue) {
x = newValue
}
})
console.log(obj) // {}
console.log(obj.x) // 100
configurable
- 是否可以通过 delete 删除并重新定义
- 是否可以修改其他属性描述符配置
- 是否可以修改 get set
const obj = { x: 100 }
Object.defineProperty(obj, 'y', {
value: 200,
configurable: false, // false
})
Object.defineProperty(obj, 'z', {
value: 300,
configurable: true,
})
delete obj.y // 不成功
// 重修修改 y 报错(而修改 z 就不报错)
Object.defineProperty(obj, 'y', {
value: 210
})
writable
属性是否可以被修改
const obj = { x: 100 }
Object.defineProperty(obj, 'x', {
writable: false,
})
obj.x = 101
obj.x // 依然是 10
Object.freeze()
冻结对象:1. 现有属性值不可修改;2. 不可添加新属性;
const obj = { x: 100, y: 200 }
Object.freeze(obj) // 冻结属性
obj.x = 101
obj.x // 100
Object.getOwnPropertyDescriptor(obj, 'x') // { configurable: false, writable: false }
obj.z = 300 // 不成功。不能再添加新属性
Object.isFrozen(obj) // true
对比 Object.seal()
密封对象:1. 现有属性值可以修改;2. 不可添加新属性;
const obj = { x: 100, y: 200 }
Object.seal(obj)
Object.getOwnPropertyDescriptor(obj, 'x') // { configurable: false }
obj.x = 101 // 成功
obj.z = 300 // 不成功。不能再添加新属性
Object.isSealed(obj) // true
enumerable
是否可以通过 for...in
遍历到。
const obj = { x: 100 }
Object.defineProperty(obj, 'y', {
value: 200,
enumerable: false, // false
})
Object.defineProperty(obj, 'z', {
value: 300,
enumerable: true,
})
for (const key in obj) {
console.log(key) // 'x' 'z'
}
console.log('y' in obj) // true —— 只能限制 for...in 无法限制 in
原型的属性描述符
在 N 年之前,使用 for...in
遍历对象时,需要用 hasOwnProperty
剔出原型属性,否则会把原型属性过滤出来。
const obj = { x: 100 }
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key)
}
}
现在不用了,我们可以把原型上的属性设置为enumerable: false
,那么就不会遍历了,也就是可以通过 enumerable
来判断是否遍历。
Object.getOwnPropertyDescriptor(obj.__proto__, 'toString') // enumerable: false