JS对象-封装
摘要:
- ES6之前的封装-构造函数
- ES6之后的封装-class
前期准备
先来理解一些最基本的概念:
(一)
// 1.构造函数
function Cat(name) {
this.name
}
// 2.构造函数原型对象
// Cat.prototype
// 3.使用Cat构造函数创建的实例‘乖乖’
var guaiguai = new Cat('guaiguai')
// 4.构造函数的静态方法,名为fn
Cat.fn = function() {}
// 5.原型对象上的方法,名为fn
Cat.prototype.fn = function () {}
实例原型链:
guaiguai.__proto__ ----> Cat.prototype -----> Object.prototype -----> null
(二)
语法糖的意思是现有技术本可以实现,但是采用某种写法会更加简洁优雅。
比如class就是语法糖。
封装
把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口。
1、ES6之前的封装
都知道ES6的class实际就是一个语法糖,那么在ES6之前,是没有类这个概念的,因此是借助于原型对象和构造函数来实现的。
- 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
- 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
- 静态属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())
1.1题目一
(理解私有属性方法和公有属性方法)
比如我现在想要封装一个生产出猫,名为Cat的构造函数。
-
由于猫的'心'和'胃'都是我们肉眼看不见的,所以我把它们设置为私有属性(隐藏起来)
-
并且猫的'心跳'我们也是看不到的,所以我把它设置为私有方法(隐藏起来)
-
然后猫的'毛色'是可以看见的,所以我把它设置为公有属性
-
并且猫的'跳起来'这个动作我们是看得到的,所以我把它设置为公有方法
function Cat(name, color) { var heart = '心' var stomach = '胃' var heartbeat = function() { console.log(heart + '跳') } this.name = name this.color = color this.jump = function() { heartbeat() // 能跳起来表明这只猫是活的,心也就能跳 console.log('我跳起来了,来追我啊') } } var guaiguai = new Cat('guaiguai', 'white') console.log(guaiguai) guaiguai.jump()
上述代码打印出来的应该是:
Cat{ name: 'guaiguai', color: 'white', jump: function(){} }
心跳
我跳起来了,来追我啊
可以看到,我们生产出名字叫乖乖的小猫咪只有这几个属性能访问到(也就是能被肉眼看到),为公有属性:
- name
- color
- jump
而私有属性,是我们看不到的:
- heart
- stomach
- heartbeat
所以如果你想要直接使用它是不能的:
// 私有
console.log(guaiguai.heart) // undefined
console.log(guaiguai.stomach) // undefined
guaiguai.heartbeat() // 报错
小结:
很好区分:
- 在函数内用var 定义的就是私有的
- 在函数内用this承接的就是共有的
1.2题目二
(理解静态属性方法和公有属性方法)
我们现在往刚刚的Cat构造函数中加些东西。
-
我们需要对Cat这个构造函数加一个描述,表明它是用来生产猫的,所以我把descript设置为它的静态属性
-
由于一听到猫这种动物就觉得它会卖萌,所以我把卖萌这个动作设置为它的静态方法
-
由于猫都会用唾液清洁身体,所以我把清洁身体这个动作设置为它的公有方法
// 这段是旧代码 function Cat(name, color){ var heart = '心' var stomach = '胃' var heartbeat = function() { console.log(heart + '跳') } this.name = name this.color = color this.jump = function() { heartbeat() console.log('我跳起来了,来追我啊') } } // 这段是新增的代码 Cat.descript = '我这个构造函数是用来生产出一只猫的' Cat.actingCute = function() { console.log('一听到猫我就想到了它会卖萌') } Cat.prototype.cleanTheBody = function() { console.log('我会用唾液清洁身体') } var guaiguai = new Cat('guaiguai', 'white') console.log(Cat.descript) Cat.actingCute() console.log(guaiguai.descript) guaiguai.cleanTheBody()
上述代码打印出来的应该是:
我这个构造函数是用来生产出一只猫的
一听到猫我就想到了它会卖萌
undefined
我会用唾液清洁身体
可以看到,我们定义的descript和actingCute是定义在构造函数Cat上的,所以可以直接被Cat调用,为静态属性和方法。
但是descript和actingCute并不能存在于'guaiguai'这个实例上,descript只是对构造函数Cat的描述,并不是对‘guaiguai’的描述,所以打印出undefined。
不过‘清洁’身体是定义在原型对象prototype中的,属于公有方法(实例方法),也就是‘guaiguai’这个实例可以用它来调用。
静态属性和方法:
- descript
- actingCute
实例(公有)属性和方法:
- name
- color
- jump
- cleanTheBody
小结:
也很好区分:
- 在构造函数上也就是Cat.xxx定义的是静态属性和方法
- 在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx,就是公有属性和方法(实例方法)
(也有小伙伴可能会有疑问,这个静态属性和方法是有什么用啊,感觉我们编码的时候并没有用到过啊。Really? 哈哈, Promise.all()、Promise.race()、Object.assign()、Array.from()这些不都是吗?)
(至于实例方法,想想push、shift,实际上不是存在原型对象上的吗?Array.prototype.push)
1.3题目三
(理解实例自身的属性和定义在构造函数原型对象中的属性的区别)
function Cat(name) {
this.name = name
}
Cat.prototype.prototypeProp = '我是构造函数原型对象上的属性'
Cat.prototype.cleanTheBody = function() {
console.log('我会用唾液清洁身体')
}
var guaiguai = new Cat('guaiguai')
console.log(guaiguai)
console.log(guaiguai.name)
console.log(guaiguai.prototypeProp)
guaiguai.cleanTheBody()
结果:
Cat {name: "guaiguai"}
guaiguai
我是构造函数原型对象上的属性
我会用唾液清洁身体
看到没,name是使用this.xxx = xxx的形式定义的,它能直接让实例guaiguai就拥有这个属性。
而prototypeProp、cleanTheBody毕竟是定义在构造函数原型上的,所以并不能出现在实例guaiguai上,但是guaiguai却能访问和调用它们。
因此我们得出结论:
定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们
1.4题目四
既然我们已经知道了实例自身的属性和定义在构造函数原型对象上的属性的区别,那我们一般怎么区别呢?
function Cat(name) {
this.name = name
}
Cat.prototype.prototypeProp = '我是构造函数原型对象上的属性'
Cat.prototype.cleanTheBody = function() {
console.log('我会用唾液清洁身体')
}
var guaiguai = new Cat('guaiguai')
for(let key in guaiguai) {
if(guaiguai.hasOwnProperty(key)) {
console.log('我是自身属性', key)
} else {
console.log('我不是自身属性', key)
}
}
console.log('-分隔符-')
console.log(Object.keys(guaiguai))
console.log(Object.getOwnPropertyNames(guaiguai))
这道题中,分别用了三种方式来获取实例对象guaiguai上的属性名:
- for ... in ...
- Object.keys()
- Object.getOwnPropertyNames()
输出的结果为:
我是自身属性 name
我不是自身属性 prototypeProp
我不是自身属性 cleanTheBody
-分隔符-
["name"]
["name"]
由此可以得出:
- 使用for... in...能获取到实例对象自身的属性和原型链上的属性
- 使用Object.keys() 和 Object.getOwnPropertyNames()只能获取实例对象自身的属性
- 可以通过.hasOwnProperty()方法传入属性名来判断一个属性是不是实例自身的属性
(上面的说法其实不太严谨,因为要建立在可枚举属性的前提下(属性的enumerable)为true)
1.5题目五
下面再做道题,看看你到底有没有掌握上面的知识点。
function Person(name, sex) {
this.name = name
this.sex = sex
var evil = '我很邪恶'
var pickNose = function() {
console.log('我会抠鼻子但不让你看见')
}
this.drawing = function(type) {
console.log('我要画一幅' + type)
}
}
Person.fight = function() {
console.log('打架')
}
Person.prototype.wc = function() {
console.log('我是个人我会wc')
}
var p1 = new Person('张三', 'boy')
console.log(p1.name)
console.log(p1.evil)
p1.drawing('国画')
p1.pickNose()
p1.fight()
p1.wc()
Person.fight()
Person.wc()
console.log(Person.sex)
答案:
张三
undefined
我要画一幅国画
Uncaught TypeError: p1.pickNose is not a function
Uncaught TypeError: p1.fight is not a function
我是个人我会wc
打架
Uncaught TypeError: Person.wc is not a function
undefined
解析:
- name 为公有属性,实例访问它打印出'张三'
- evil 为私有属性,实例访问它打印出'undefined'
- drawing是公有(实例)方法,实例调用它打印出'我要画一幅国画'
- pickNose是私有方法,实例调用它会报错,因为它并不存在于实例上
- fight是静态方法,实例调用它报错,因为它并不存在于实例上
- wc存在于构造函数的原型对象中,使用实例调用它打印出'我是个人我会wc'
- fight存在于构造函数上,使用构造函数调用它打印出'打架'
- wc存在于构造函数的原型对象中,并不存在于构造函数中,所以报错
- sex为公有(实例)属性,并不存在于构造函数上,使用构造函数访问它为undefined
1.6题目六
如果我的构造函数和构造函数原型对象上存在相同名称的属性咋办呢?
function Cat() {
this.color = 'white'
this.getColor = function() {
console.log(this.color)
}
}
Cat.prototype.color = 'black'
var cat = new Cat()
cat.getColor()
这里的执行结果为:
white
这个其实很好理解:当查找一个属性/方法时,它肯定会先在当前对象查找,当前对象中找不到时,才会到原型对象中查找,或者再到该对象的原型的原型,一层一层向上查找,直到找到一个名字匹配的属性/方法或到达原型链的末尾(null),这也就是原型链查找。
1.7题目七
现在我在Cat的原型对象上,还有它原型对象的原型对象上都定义一个叫color的属性。
(原型对象本质也是个对象,所以它的__proto__也就是Object.prototype)
function Cat() {
this.color = 'white'
this.getColor = function() {
console.log(this.color)
}
}
Cat.prototype.color = 'black'
Object.prototype.color = 'yellow'
Object.prototype.feature = 'cute'
var cat = new Cat()
cat.getColor()
console.log(cat)
console.log(cat.feature)
然后我们来看看结果:
white
Cat{color: 'white', getColor: f}
cute
可以看到:
color这个属性还是以它自身的white为主,但是feature这个属性没在实例cat上吧,所以它就会向上层一层查找,结果在Object.prototype中找到了,因此打印出cute。
1.8
比如下面这种写法:
var obj = { name: 'obj' }
console.log(obj.toString())
console.log(obj.hasOwnProperty('name'))
console.log(Object.prototype)
为什么我的obj中明明就没有toString()、hasOwnProperty()方法,但是我却可以调用它。
现在我知道了,原来obj本质是个Object类型。
使用var obj = { name: 'obj' }就相当于是调用了new Object:
var obj = new Object({name: 'obj'})
这样的话,我当然就可以使用Object.prototype上的方法了。
总结-构造函数
现在回头看看那句话:
把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口
是不是好理解多了呢?
然后让我们对构造函数配合原型对象封装来做一个总结吧:
(一)私有属性、公有属性、静态属性概念:
- 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
- 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
- 静态属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(Object.assign())
(二)实例对象上的属性和构造函数原型上的属性:
- 定义在构造函数原型对象上的属性和方法虽然不能直接表现在实例对象上,但是实例对象却可以访问或者调用它们。
- 当访问一个对象的属性/方法时,它不仅仅在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,一层一层向上查找,知道找到一个名字匹配的属性/方法或到达原型链的末尾(null)
(三)遍历实例对象属性的三种方法:
- 使用for...in...能获取到实例对象自身的属性和原型链上的属性
- 使用Object.keys()和Object.getOwnPropertyNames()只能获取对象自身的属性
- 可以通过.hasOwnProperty()方法传入属性名来判断一个属性是不是实例自身的属性
** 2、ES6之后的封装 **
在ES6之后,新增了class这个关键字。
它可以用来代替构造函数,达到创建‘一类实例’的效果。
并且类的数据类型就是函数,所以用法上和构造函数很像,直接用new 命令来配合它创建一个实例。
还有一件事你可能不知道吧,那就是,类的所有方法都定义在类的prototype属性上面。
class Cat{
constructor() {}
toString() {}
toValue() {}
}
// 等同于
function Cat() {}
Cat.prototype = {
construcor() {}
toString() {}
toValue() {}
}
2.1题目一
现在我们将1.1的题目换成class版本的来看看。
class Cat{
constructor(name, color) {
var heart = '心'
var stomach = '胃'
var heartbeat = function() {
console.log(heart + '跳')
}
this.name = name
this.color = color
this.jump = function() {
heartbeat()
console.log('我跳起来了,来追我啊')
}
}
}
var guaiguai = new Cat('guaiguai', 'white')
console.log(guaiguai)
guaiguai.jump()
其实你会发现,当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回,因此它被称为constructor构造方法(函数)。
(另外,其实如果你的class没有定义constructor,也会隐式生成一个constructor方法)
可以看到,经过class改造后的Cat
公有(实例)属性和方法:
- name
- color
- jump
而对于私有属性,个人感觉上述的heart不应该叫做私有属性,它只不过被局限于constructor这个构造函数中,是这个作用域下的变量而已。
执行结果:
Cat{ name: 'guaiguai', color: 'white', jump: function () {} }
心跳
我跳起来了,来追我啊
2.2题目二
(弄懂在类中定义属性或方法的几种方式)
class Cat {
constructor() {
var heart = '心'
this.name = 'guaiguai'
this.jump = function() {}
}
color = 'white'
cleanTheBody = function() {
console.log('我会用唾液清洁身体')
}
hideTheShit() {
console.log('我在臭臭完之后会把它藏起来')
}
}
var guaiguai = new Cat()
console.log(guaiguai)
console.log(Object.keys(guaiguai))
guaiguai.cleanTheBody()
guaiguai.hideTheShit()
请仔细看看这道题,在这里我用了四种不同的方式来定义一些属性。
1.在constructor中var一个变量,它只存在于constructor这个构造函数中。
2.在constructor中使用this定义的属性和方法会被定义到实例上。
3.在class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上。
4。在class中直接定义一个方法,会被添加到原型对象prototype上。
至此,这道题的答案为:
Cat {color: 'white', name: 'guaiguai', cleanTheBody: f, jump: f}
['color', 'cleanTheBody', 'name', 'jump']
我会用唾液清洁身体
我在臭臭完之后会把它藏起来
这四种定义的方式已经介绍完了,相信大家比较迷惑的一点就是以下这两种方式的定义吧:
class Cat {
cleanTheBody = function() {}
hideTheShit() {}
}
看起来都是定义一个函数,为什么第一个就可以在实例对象中,而第二个是在原型对象中呢?
其实不需要特意的去记住它,你只需要知道:在类中的所有方法都是定义在类的prototype属性上面。
这里的cleanTheBody你可以理解为它和color一样只是一个普通的变量,只不过这个变量是个函数,所以它并不算是定义在类上的函数,因此不会存在于原型对象上。
而hideTheShit是实实在在的定义在类上的方法,所以它和constructor方法一样,都是在类的原型对象上。
转化为伪代码就是:
class Cat {
constructor() {}
hideTheShit() {}
}
// 等同于
function Cat() {}
Cat.prototype = {
constructor() {}
hideTheBody() {}
}
2.3题目三
(在class定义静态属性和方法)
前面我们给Cat定义静态属性和方法是采用这种方式,Cat.xxx:
function Cat() {...}
Cat.descript = '我这个构造函数是用来生产出一只猫的'
Cat.actingCute = function() {
console.log('一听到猫我就想到了它会卖萌')
}
在class中你也可以使用Cat.xxx这种方式定义,因为前面说过了,class本质也是个对象。
但除此之外,你还可以使用static标识符表示它是一个静态的属性或方法:
class Cat {
static descript = '我这个类是用来生产出一只猫的'
static actingCute() {
console.log('一听到猫我就想到了它会卖萌')
}
}
现在让我们来做做下面这道题吧:
class Cat {
constructor(name, color) {
var heart = '心'
var stomach = '胃'
var heartbeat = function() {
console.log(heart + '跳')
}
this.name = name
this.color = color
heartbeat()
this.jump = function() {
console.log(this)
console.log('我跳起来了,来追我啊')
}
}
cleanTheBody = function() {
console.log('我会用唾液清洁身体')
}
static descript = '我这个类是用来生产一只猫的'
static actingCute() {
console.log(this)
console.log('一听到猫我就想到了它会卖萌')
}
}
Cat.staticName = 'staticName'
var guaiguai = new Cat('guaiguai', 'white')
console.log(guaiguai)
guaiguai.jump()
guaiguai.cleanTheBody()
console.log(guaiguai.descript)
guaiguai.actingCute()
Cat.actingCute()
console.log(Cat.descript)
console.log(Cat.staticName)
结果:
心跳
Cat{ name: 'guaiguai', color: 'white', jump: function(){}, cleanTheBody: function(){} }
Cat{ name: 'guaiguai', color: 'white', jump: function(){}, cleanTheBody: function(){} }
'我跳起来了~来追我啊'
'我会用唾液清洁身体'
undefined
Uncaught TypeError: guaiguai.actingCute is not a functionclass
Cat{...}
'一听到猫我就想到了它会卖萌'
'我这个类是用来生产出一只猫的'
'staticName'
结果分析:
-
首先在构造guaiguai这个对象的时候会执行heartbeat方法,打印出心跳
-
其次打印出的guaiguai它只会拥有class中定义的实例属性和方法,所以并不会有descript和actingCute
-
jump中的this指向的是实例对象guaiguai,并且执行了'我跳起来了~来追我啊'
-
直接定义在class中的属性或者方法就相当于是定义在实例对象上,所以也属于实例方法,cleanThebody会执行打印出'我会用唾液清洁身体'
-
使用了static定义的属性和方法为静态属性和方法,并不存在于实例上,所以打印出undefined和报错
-
actingCute使用了static修饰符,所以它是静态方法,存在于Cat这个类上,因此它里面的this指向这个类,并且执行了'一听到猫我就想到了它会卖萌'
-
descript使用了static修饰符,所以它是静态属性,打印出'我这个类是用来生产出一只猫的'
-
Cat.staticName = 'staticName'就相当于定义了一个静态属性,所以打印出staticName
2.4题目四
我们再来看看这道题,友情提示,这是个坑。。
var a = new A()
function A() {}
console.log(a)
var b = new B()
class B {}
console.log(b)
你开始的预想是不是:
A{}
B{}
结果却发现报错了:
A{}
Uncaught ReferenceError: Cannot access 'B' before initialization
那是因为,函数A是会被提升至作用域的最顶层,所以可以在定义函数A之前使用new A ()
但是类却不存在这种提升机制,所以当你执行new B() 的时候它就会告诉你在B没有初始化之前不能使用它。
尽管我们知道,class它的本质也是一个函数:
console.log(typeof B) //function
2.5题目五
坑二。。
class Cat {
constructor() {
this.name = 'guaiguai'
var type = 'constructor'
}
type = 'class'
getType = function() {
console.log(this.type)
console.log(type)
}
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
答案为:
class
window
解析:
- 调用getType函数的是guaiguai,所以里面的this指向了guaiguai,而guaiguai上的type为class。
- 当要打印type的时候,发现getType函数中并没有这个变量,所以就向外层查找,找到了window中存在这个变量,因此打印出window。(var type = 'constructor'是函数constructor中的变量,你也可以理解为constructor函数的私有变量)
2.6题目六
既然做到了函数类型的题目,那怎么办能不想到箭头函数呢?
让我们将2.5中getType函数换成箭头函数看看?
class Cat{
constructor () {
this.name = 'guaiguai'
var type = 'constructor'
}
type = 'class'
getType = () => {
console.log(this.type)
console.log(type)
}
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
console.log(guaiguai)
现在调用guaiguai.getType()你觉得会是啥?
“既然箭头函数内的this是由外层作用域决定的,那这里外层作用域是window,当然this.type就是window咯”
当然不对,还记得我之前说过的,class的本质是个函数吗?所以当你碰到class内有箭头函数的题目,把它当成构造函数创建对象来处理就可以了。
在构造函数中如果使用了箭头函数的话,this指向的就是这个实例对象。
因此将class转化为构造函数的话,伪代码为:
function Cat() {
this.type = 'class'
this.getType = () => {
console.log(this.type)
console.log(type)
}
}
Cat.prototype.constructor = function() {
this.name = 'guaiguai'
var type = 'constructor'
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.constructor()
guaiguai.getType()
console.log(guaiguai)
别的都好理解,这里为啥,constructor要放在原型对象中,并且要在var guaiguai = new Cat()下面再调用它呢?
还记得在2.2中我们就说过吗,任何放在类上的方法都相当于写在原型对象上,并且在使用类的使用,会隐式执行constructor函数。这两段代码就是为了模拟这个操作。
这样的话,上面两个题目的结果都是:
class
window
Cat {type: "class", name: "guaiguai", getType: ƒ}
2.7题目七
如果class中存在两个相同的属性或者方法会怎么样?
class Cat {
constructor() {
this.name = 'cat1'
}
name = 'cat2'
getName = function() {
console.log(this.name)
}
}
var cat = new Cat()
cat.getName()
这道题中,我们调用getName方法,打印出的会是:
cat1
所以可以看出constructor中定义的相同名称的属性和方法会覆盖在class里定义的。
2.8题目八
那么,原型对象中相同名称的属性和方法呢?
class Cat{
constructor() {
this.name = 'cat1'
}
name = 'cat2'
getName = function() {
console.log(this.name)
}
}
Cat.prototype.name = 'cat3'
var cat = new Cat()
cat.getName()
答案:
cat1
没错,还是以constructor中的为准。这里和构造函数中同名属性的处理方式是一样的。
2.9题目九
现在可以加大难度了:
class Cat{
constructor() {
this.name = 'guaiguai'
var type = 'constructor'
this.getType = () => {
console.log(this.type)
console.log(type)
}
}
type = 'class'
getType = () => {
console.log(this.type)
console.log(type)
}
}
var type = 'window'
var guaiguai = new Cat()
guaiguai.getType()
console.log(guaiguai)
首先我们很清楚,如果console.log(type)打印出的是window那就表示使用的是第二个getType,否则表示用的是第一个getType。
那么根据题2.7,我们可以看出,第一个getType是会覆盖第二个的,所以执行结果为:
class
constructor
Cat {type: "class", name: "guaiguai", getType: ƒ}
总结-class
(一)class的基本概念
- 当你使用class的时候,它会默认调用constructor这个函数,来接收一些参数,并构造出一个新的实例对象(this)并将它返回。
- 如果你的class没有定义constructor,也会隐式生成一个constructor方法。
(二)class中几种定义属性的区别:
- 在constructor中var一个变量,它只存在于constructor这个构造函数中
- 在constructor中使用this定义的属性和方法会被定义到实例上
- 在class中使用=来定义一个属性和方法,效果与第二点相同,会被定义到实例上
- 在class中直接定义一个方法,会被添加到原型对象prototype上
- 在class中使用static修饰符定义的属性和方法被认为是静态的,被添加到类本身,不会添加到实例上
(三)other
- class本质虽然是个函数,但是并不会像函数一样提升至作用域最顶层
- 如遇class中箭头函数等题目请参照构造函数来处理
- 使用class生成的实例对象,也会有沿着原型链查找的功能

浙公网安备 33010602011771号