《你不知道的JavaScript》阅读笔记(3)

105 阅读7分钟

《你不知道的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也会出现屏蔽效应