你不知道的JavaScript-上卷 读书笔记-part2

书评

豆瓣
这本书很适合初级前端开发者上升至中级前端开发者,很好的阐述了JavaScript的闭包,原型,类,编译,赋值的问题.而且这还是上卷,还会有中卷,下卷,等等之类的.我会从这本书里选取一些比较重要的内容放在这篇文章当中(实际上这本书全部内容都重要). let’s do it

对象

类型

JavaScript有六种主要类型

  • string
  • number
  • boolean
  • null
  • undefined
  • object

简单基本类型(string,number,boolean,null,undefined)本身并不是对象,null有时候会被当做一种对象类型,但其实这是一个语言本身的bug,typeof null 的时候会返回字符串”object”,实际上null本身是基本类型.
JavaScript万物并不都是对象.
JavaScript本身有许多特殊的对象子类型,我们可以称之为复杂基本类型.
函数就是对象的一个子类型.数组也是对象的一种类型,具备一些额外的行为.

JavaScript内置对象

JavaScript还有一些对象子类型,通常被称为内置对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在JavaScript中,这些内置函数可以当做构造函数.JavaScript在访问对象属性的时候会自动把字符串字面量转换为一个对象 比如

1
2
var str="hello"
str.length;//转换为String对象

之所以能够访问到属性和方法,是因为引擎把字面量转换为String对象,所以可以访问属性和方法.

内容

存储在对象容器内部的是这些属性的名称,他们就像指针(从技术角度来说就是引用)一样,指向这些值的真正存储位置.
实际上这里要补充一些

1
2
3
4
5
var b={a:1};
var c=b.a;
c=4;
console.log(b);//{a:1}
console.log(c);//4

首先我们知道基本类型,string,number,boolean这些是单独开辟一个空间存储的 就算引用也不是说引用地址而是直接开辟一块新的空间,但是对象不同了

1
2
3
4
5
var s={a:233};
var vv=s;
vv.a=555;
console.log(vv.a);//555
console.log(s.a);//555

对象仅仅是引用,包括你在函数中传参也是一样的

1
2
3
4
5
6
var obj={a:1}
function setObj(o){
o.a=444;
}
setObj(obj)
console.log(obj);//{a:444}

在对象中,属性名永远都是字符串,如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串.即使是数字也不例外.虽然在数组下标中使用的的确是数组,但是在对象属性名中数字会被转换为字符串

属性与方法

从技术角度来说,函数永远不会”属于”一个对象,所以把对象内部引用的函数称为”方法”似乎有点不妥.就算是this也仅仅是在运行的时候根据调用位置动态绑定的.所以函数与对象的关系最多也就是简介关系.
即使在对象中声明一个函数表达式,这个函数也不会”属于”这个对象——它们只是对于相同函数的多个引用.

1
2
3
4
5
6
7
8
var myObject={
foo:function(){
console.log("foo")
}
};
var someFoo=myObject.foo;
someFoo;//function foo(){}
myObject.foo;//function foo(){}

数组

以字符串的形式往数组中添加属性length是不会变的.但是属性会存在

1
2
3
4
var myArray=["hello",2,"world"];
myArray.bar="bar"
myArray.length;//3
myArray.bar;//bar

对象的复制

通过上面我们可以看到,js的对象默认都是引用的,但是基本属性都是直接开辟内存不会去引用.来看这一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var b={arr:[1,2,3,4,5],s:"str",bool:true}
var c=b.arr;
c.push(6);
b;//{arr:[1,2,3,4,5,6],s:"str",bool:true}
c;//[1,2,3,4,5,6]
var z={};
for(var k in b){
z[k]=b[k]
}
z.s="zStr";
z.bool=false;
z.arr.push(7);
z;//{arr:[1,2,3,4,5,6,7],s:"zStr",bool:false}
c;//[1,2,3,4,5,6,7]
b;//{arr:[1,2,3,4,5,6,7],s:"str",bool:true}

但是如果我们直接访问对象中的属性,那么访问到的就是地址而不是引用了.再来看一段代码

1
2
3
4
5
6
7
8
9
10
var b={arr:[1,2,3,4,5],s:"str",bool:true}
var over={};
for(var k in b){
if(Object.prototype.toString.call(b[k])=="[object Array]"){
b[k].forEach(function(e,i){over[k]=[];over[k][i]=e})
}
}
over.arr.push(666)
over.arr;//arr:[1,2,3,4,5,666]
b.arr;//arr:[1,2,3,4,5]

还有一种复制方法,那就是利用JSON来解析出一个一模一样的对象,但是这样做必须要保证这段JSON文件是安全的.

1
2
3
4
var newObj=JSON.parse(JSON.stringify(b))
newObj.arr.push(7777);
newObj.arr;//arr:[1,2,3,4,5,7777]
b.arr;//arr:[1,2,3,4,5]

存在性

我们可以在不访问属性值的情况下判断对象中是否存在这个属性

1
2
3
4
5
6
7
8
9
var myObject={
a:2
};
("a" in myObject);//true
("b" in myObject);//false
myObject.hasOwnProperty("a");//true
myObject.hasOwnProperty("b");//false

in操作符会检查属性是否在对象以及[[Prototype]]原型链中.相比之下,hasOwnProperty(…)只会检查属性是否在myObject对象中,不会检查[[prototype]]链

类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域建模方法.
面向对象编程强调的是数据与操作数据行为本质上是互相关联的(当然,不同的数据有不同的行为),因此好的设计就是把数据以及和它相关的行为打包(或者说封装)起来,这在正式计算机科学中有时被称为数据结构.

举例来说,用来表示一个单词或者短语的一串字符通常被称为字符串,字符就是数据,但是你关心的往往不是数据是什么.而是可以对数据做什么.所有可以应用在这种数据上的行为(计算长度,添加数据,搜索)都被设计成String类的方法.
所有字符串都是String类的一个实例,也就是说它是一个包裹(封装),包含字符数据和我们可以应用在数据上的函数.

“汽车”可以被看做”交通工具”的一种特例,后者是更广泛的类.
我们可以在软件中定义一个Vehicle类和Car类来对这种关系进行建模.

Vehicle的定义可能是包含推进器(引擎),载入能力等等,这些都是Vehicle的行为.我们在Vehicle中定义的是(几乎)所有类型的交通工具(飞机,火车,汽车)都包含的东西.在我们的软件中,对不同交通工具重复定义”载入能力”是没有意义的.相反,我们只在Vehicle中定义一次,定义Car时,只要声明它继承(或者扩展)了Vehicle这个基础定义就行,Car的定义就是对通用Vehicle定义的特殊化.

虽然Vehicle和Car会定义相同的方法,但是在实例中的数据可能是不同的,比如每辆车独一无二的VIN(车辆识别号码)

类的另一个核心概念就是多态.这个概念是说父类的通用行为可以被子类用更特殊的行为重写.实际上相对多态性允许我们重写行为中引用基础行为
类也是一种设计模式.
一个类就是一张蓝图.为了获得真正可以交互的对象,我们必须按照类来建造(实例化)一个东西,这个东西通常被称为实例,有需要的话,我们可以直接在实例上调用方法并访问其所有的公共数据属性

写到这里,我突然想到了对自己某次开发实践中的经验补充,那次实践是做一个筛选栏(下拉栏)然后根据下拉的选项不同来获得不同的地图数据
首先那些下拉框都是一个大类把它定义为PullDown
这个PullDown有一些prototype方法.
PullDown.protorype.Onchange=function(){}
那么每一个下拉框都是一个子类
每一个子类都有这个Onchange方法.当子类变动的时候可以把this.val存储起来到子类的属性中
那么搜索按钮是一个单独类 因为它只有click事件不用去处理变动问题
当它点击了click事件,那么就遍历PullDown的子类获取到所有的val 然后通过搜索按钮类的PULLAJAX方法来获取结果 获取到结果后再把结果显示到地图显示类(showMapClass)
地图显示类也有一些方法 比如根据click传递过来的参数显示地图
中间的数据交换可以用PUB-SUB方式来获取数据.
如果当中牵扯到很多的数据交换 那么可以用中介者或者单独用个类来保存数据
遍历PullDown子类可以用一个工厂模式 每次新建一个子类的时候都保存到工厂模式的 allObject中
从这里我发现了一个问题就是 以后设计软件不能直接动手干了 要先分析然后再动手
如果就是几十行代码的一个文件就没有必要用类了..因为会带来很多不必要的代码
所以还是要分析。。
而分析这块..我还是处于空白状态。。。

JavaScript中函数无法(用标准,可靠的方法)真正的复制,所以只能复制对共享函数的引用(函数就是对象),如果你修改了共享函数对象,比如添加了一个属性,那么引用这个函数的所有变量/对象都将受到影响

原型

前段时间自己写了一篇文章稍微分析了一下可以参考

JavaScript访问属性的时候会调用一个[[get]]方法,当这个方法在当前对象找不到该属性那么就会顺着[[prototype]]向上一级对象去找.
用in操作符来检查属性在对象中是否存在时,同样会查找对象整条原型链(无论属性是否可枚举)

Object.prototype

所有普通的[[prototype]]链最终都会指向内置的Object.prototype
所有我们才能在没有那些toString方法的实例上调用这个方法

JavaScript中的类

JavaScript只有对象.

类函数

1
2
3
4
5
6
7
function Foo(){
}
var a=new Foo();
Object.getProtorype===Foo.Prototype;//true

在面向类的语言中,类可以被复制多次,而在JavaScript中,没有类似复制的机制.你不能创建一个类的多个实例,只能创建多个对象,他们的[[prototype]]是同一个对象.但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的.

new Foo()会生成一个新对象(这里称为a),这个新对象的内部链接[[prototype]]关联的是Foo.prototype对象.
最后我们得到两个对象,它们之间相互关联.就这样,我们没有初始化一个类,实际上我们并没有从”类”中复制任何行为到一个对象中,只是让两个对象相互关联.
实际上new Foo()这个函数调用实际上没有直接创建关联,这个关联只是一个意外的副作用,但是间接的完成了我们的目的:一个关联到其他对象的新对象

构造函数

1
2
3
4
5
6
function Foo(){}
Foo.prototype.constructor===Foo;//true
var a=new Foo();
a.constructor===Foo;//true

Foo.prototype默认有一个公用并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数.实际上a本身没有这个.constructor属性,虽然a.constructor的确指向Foo函数,但是这个属性并不表示a由Foo”构造”.
实际上Foo和程序中的其他函数没有任何区别,函数本身不是构造函数,然而,当你在普通的函数调用前面加上new关键词后,就会把这个函数调用变成一个”构造函数调用”.实际上,new会劫持所有的普通函数并且构造对象的形式来调用它.
换句话说,在JavaScript中对于构造函数的最准确解释是:所有带new的函数调用.函数不是构造函数,但是当且仅当使用new时,函数调用会变成”构造函数调用”

constructor

1
2
3
4
5
function Foo(){}
Foo.prototype={};//创建一个新原型对象
var a1=new Foo();
a1.constructor===Foo;//false
a1.constructor===Object;//true

a1本身没有这个.constructor属性.于是会委托到[[prototype]]上的Foo.prototype.但是这个对象也没有.constructor属性(不过默认Foo.prototype对象有这个属性),所以会继续委托,这次会委托给链顶端的Object.prototype这个对象有.constructor属性.即指向Object()函数
要修复这个问题只需要在Foo.prototype加上这个属性即可

1
2
3
4
5
6
Object.defineProperty(Foo.prototype,"constructor",{
enumerable:false,//不可枚举
writable:true,//可写
configurable:true,
value:Foo
})

实际上对象的.constructor属性会默认指向一个函数,这个函数可以通过对象的.prototype引用.但是最好记住一点”constructor并不表示被构造”

原型继承

下面来写一段典型的原型风格继承

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
function Foo(name){
this.name=name;
}
Foo.prototype.myName=function(){
return this.name
}
function Bar(name,label){
Foo.call(this,name)
this.label=label;
}
//我们创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype=Object.create(Foo.prototype);
//注意!现在没有Bar.prototype.constructor了
//如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel=function(){
return this.label
}
var a=new Bar("a","obj a");
a.myName();//"a"
a.myLabel();//"obj a"

用 Object.create()会凭空创建一个”新”对象并把新对象内部的[[prototype]]关联到你指定的对象(本例中是Foo.prototype);

1
2
3
4
5
//和你想要的机制不一样
Bar.prototype=Foo.prototype;
//基本满足你的需求,但是可能会产生一些副作用
Bar.prototype=new Foo();

Bar.prototype=Foo.prototype并不会创建一个关联到Bar.prototype的新对象它只是让Bar.prototype直接引用Foo.protorype对象,因此如果修改Bar.protorype属性如添加方法或者删除属性,都会直接修改Foo.prototype本身.
Bar.protorype=new Foo()的确会创建一个关联到Bar.prototype的新对象.但是它使用了Foo()的”构造函数调用”,如果函数Foo有一些副作用(比如写日志,修改状态,注册到其他对象.给this添加数据属性..)就会影响到Bar()的后代.
因此要创建一个合适的关联对象,我们必须使用Object.create()这样做的唯一缺点就是需要创建一个新对象,然后把旧对象抛弃掉,不能直接修改已有的默认对象.

检查”类”关系

instanceof判断

1
2
3
4
5
6
7
function Foo(){
}
Foo.prototype.blah=..;
var a=new Foo();
a instanceof Foo;//true

这个方法只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系.如果你想判断两个对象(a,b)直接是否通过[[prototype]]联系,只用instanceof无法实现.

isPrototypeOf

1
Foo.prototype.isPrototypeOf(a);//true

比如判断c的原型链是否有b

1
b.isPrototypeOf(c)

这个方法不需要使用函数(“类”)直接用b与c之间的对象引用来判断它们的关系.

getProtorype

ES5的标准方法

1
Object.getProtorype(a)===Foo.protorype;//true