《你不知道的JavaScript》阅读笔记(3)
对象
JavaScript类型:string/number/boolean/null/undefined/symbol/object,除了对象Object为引用类型,其他都为基本类型,null有时会作为一个空对象,但这其实是JS的一个本身bug
对象子类型:String、Number、Boolean、Boolean、Object、Function、Array、Date、RegExp、Error,其实如果学过Java会发现跟Java中的包装类很像,实际上以上这几个自类型在JS中表现为函数,他们可以作为构造函数,通过new关键字来产生新的对象
之所以你能直接在基本类型字面量上进行一些方法的操作,其实是JS引擎帮你做了一步,就是内部通过包装类来生成了对象,在进行方法和属性的访问
var str='312321'
str.length;
对象的内容一般不会直接存放在对象容器中,对象中一般存放属性名,他就像指针一样指向内容部分,且属性名在对象中总是为字符串,即使你数组访问使用索引在内部也会转变为字符串
可计算属性:
var pre='foo'
var obj={
[pre+'aa']:1
}
obj['fooaa'] //1
数组也是对象你可以在数组上声明键值对,但我们一般不会这么做
属性描述符:对象属性描述符包括value:属性值,writable:是否可写,enumerable:是否可枚举,configurable:是否可配置
- value:不做过多讲述
- writable
const obj={}
Object.defineProperty(obj,'a',{
value:1,
writable:false
})
obj.a=3
obj.a //1
- configurable:configurable如果为false表示不能被defineProperty修改属性描述符,还会禁止删除该属性
const obj={}
Object.defineProperty(obj,'a',{
value:1,
configurable:false
})
Object.defineProperty(obj,'a',{
value:1,
configurable:true
}) //TypeError
delete obj.a //false
- enumerable:是否可枚举,如果为false那么一般在for in中就不会访问该属性
访问属性:访问属性其实并不是单纯的拿到属性的值,他会遍历对象的原型链,找到最近的一个值返回,如果没有则返回undefined
es5可以通过自定义getter和setter来改写对象的默认操作,如下
// getter
const obj={
get a(){
return 2
}
}
Object.defineProperty(obj,'b',{
get:function(){
return this.a*2
}
})
obj.b//4
//setter
const obj={
get a(){
return this._a_
},
set a(val){
this._a_=val*2
}
}
obj.a=8
obj.a//16
存在性:in和hasOwnProperty的区别为in会查找整个原型链,hasOwnProperty则不会
遍历:for in可以遍历对象上的可枚举值,for of则会访问对象的迭代器,通过迭代器来遍历对象,普通对象在JS中是没有实现迭代器的,数组、Map则有,手动调用迭代器
const a=[1,2,3]
const it=a[Symbol.iterator]()
it.next()//{ value:1, done:false }
it.next()//{ value:2, done:false }
it.next()//{ value:3, done:false }
it.next()//{ done:true }
原型
JS中的对象有个特殊的[[prototype]]内置属性,这个属性是对其他对象的引用,它也可能为空,且在当前的主流浏览器中,你可以通过__proto__这个属性来访问,这个属性并不是标准api,只不过是浏览器提供给我们的
之前我们提到过访问对象的属性会触发[[get]]操作,会在对象上寻找属性,如果没有找到,那么就会访问对象的[[prototype]]链,也就是原型链,看下面代码
const obj={
a:1
}
const obj2=Object.create(obj)
obj2
上述代码执行完后你访问obj2是个空对象,但是如果你访问obj2.a那么就会发现它是1,这是什么原因呢,因为Object.create这个函数会创建一个对象,将这个对象的[[prototype]]属性指向传入的对象,所以说当我们访问a属性触发[[set]]方法,但是obj2属性上并没有就会去obj2的原型对象上查找,如果原型对象上也没有呢?很显然会去原型对象的原型对象中查找这个样就型成了原型链
原型链的尽头对于普通的原型链来说尽头都是内置对象Object.prototype,这也是为什么你声明了一个对象,但是可以调用对象上的各种方法,就是内置的对象原型上JS提供了很多方法
屏蔽效应:如果对象本身和原型链上都有相同属性,那么会优先返回对象本身的属性,原型链上的属性则会被屏蔽
看下面一个有趣的例子
const obj={
a:1
}
const obj2=Object.create(obj);
obj2.a++;
obj.a;//1
obj2.a//2
上述代码要注意的点就是obj2.a++它其实触发了屏蔽效应,它可以写成obj2.a=obj2.a+1,那么就可以理解为obj2原型上的a加上1赋值给了obj2本身上
类函数:JS中每个函数都有一个prototype属性,它会指向一个对象,而且通过new的方法调用类函数时,创建的对象则会关联到函数的prototype属性上
function Foo(){}
const foo=new Foo;
Object.getPrototypeOf(foo)===Foo.prototype //true
function Foo(){}
const foo=new Foo();
Foo.prototype.constructor===Foo // true
foo.constructor===Foo //true
函数的原型对象上其实会有一个不可枚举的constructor属性,它指向的是对象关联的函数,创建的新对象也有个constructor属性,但这个属性本质上并不能理解为构造函数,这个属性根本上是通过对象的原型链访问的,类似于你访问constructor属性但是对象本身并没有这个属性,他就会访问对象的[[prototype]]属性
构造函数:向上述代码中你可以把Foo理解为构造函数,但是JS并没有规定什么样的函数才是构造函数,可能很多时候会认为开头字母大写的是构造函数,其实JS引擎并不会在意这个,如果硬要给JS中的构造函数一个概念的话,可以理解为通过new方法调用的函数是构造函数
判断类的关系:
instanceof 本质是判断对象的原型链有一条是指向构造函数的原型对象的,如下
function Foo(){}
const f=new Foo();
f instanceof Foo
上述代码本质上是说f的原型链上是否某个对象的[[prototype]]属性指向了Foo.prototype,而事实也是如此
我们判断一个对象是否会关联到另一个对象上有一个方法就是通过instance方法
const isRelated(o1,o2){
function F(){}
F.prototype=o2;
return o1 instanceof F
}
const obj={}
const obj2=Object.create(obj)
isRelated(obj2,obj); //true
上述isRelated函数其实就是说o1的原型链上是否某个对象的[[prototype]]属性指向了o2,可以看到obj2.proto.__proto__指向的obj所以会返回true
但其实上面的方法可以说理解起来非常的难,JS提供了标准的api来让我们判断,上面的代码改写成如下也是成立的
obj.isPrototypeOf(obj2) //true
Object.create可以创建一个干净的对象,他创建的对象可以不产生原型链,即不会产生对其他对象的委托,如下
const obj=Object.create(null)
一般对象的属性我们直接定义在对象内部,尽量不要通过原型链的方式,这会使你的代码变得较为难读懂,当然这也是视情况而定
行为委托
行为委托在JS中可以理解为当某个属性在当前对象属性中找不到时,就会去原型链上逐级找到该属性
很多地方可能会把修改构造函数的原型对象讲成是一种继承,其实从功能上来说确实没什么问题,但在JS中其实称之为委托更为合理
JS中的类理论,在一些面向对象语言中你可能见过下面的伪代码
class a{
testa(){
console.log('a')
}
}
class b extends a{
testb(){
console.log('b')
}
testa(){
super();
console.log('aa')
}
}
aa=new a();
aa.testa() // a aa
这在JS就可以如下实现
const o={
a:function(){
console.log('a')
}
}
const obj=Object.create(o)
obj.testa=function(){
this.a();
console.log('aa')
}
obj.testa()//a aa
我们通过obj的[[prototype]]属性来委托访问到原型上的a方法,实现的功能就类似继承,在JS中对象和原型对象上的命名我们会尽量不去同名,不然很会出现屏蔽效应
es6中的class语法其实本质上是原型委托的语法糖,当我们子类继承时其实并不会创建新的变量
class C{
constructor(){
this.num=Math.random()
}
rand(){
console.log(this.num)
}
}
const o1=new C()
o1.rand() //0.32121
C.prototype.rand=function(){
console.log(this.num*100)
}
const o2=new C()
o2.rand() //33.3231
o1.rand() // 23.3321
可以看到上述代码修改了C原型对象上的rand方法会把o1实例中的方法也一起修改了,其实就可以理解为实例中的方法其实也只是一个引用而已
class也会出现屏蔽效应