[关闭]
@FunC 2017-11-06T22:49:11.000000Z 字数 6881 阅读 1811

JS高程 | CH04~CH06

JavaScript


CH04 变量、作用域和内存问题

引用类型指那些可能由多个值组成的对象。

动态属性

对于引用类型的值,我们可以为其添加、删除和改变属性。但不能给基本类型的值添加属性(尽管不会报错)

复制变量值

复制基本类型的值时,会创建一个新值。复制引用类型的值时,其实只是复制了对象的指针。

检测类型

若要检测基本类型中的string, number, boolean和undefined,typeof操作符是最佳工具。但其它情况就不那么理想了(甚至在部分浏览器中,用typeof检测正则表达式会返回“function”)。
比较通用的类型检测方法是使用Object.prototype.toString.call(target)。此法在ECMASCript 5.1及以后版本中都能检测所有内置类型。

执行环境及作用域

执行环境(execution context)定义了变量或函数有权访问的其它数据,决定了它们各自的行为。每个执行环境都有一个相关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中,但我们的代码无法访问这个对象,解析器在处理数据时会在后台使用它。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。用途是保证执行环境有权访问所有的变量和函数的有序访问。作用域链的前端,始终是当前执行代码所在执行环境的变量对象。如果该环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始只包含一个变量,即arguments对象。

延长作用域链

以下两种情况下能延长作用域链:
1. try-catch语句的 catch
2. with 语句
对于with语句,会将指定对象添加到作用域链中。例如:

  1. function buildUrl() {
  2. var qs = "?debug=true";
  3. with(location) {
  4. var url = href + qs; // 此处的url等价于location.href
  5. }
  6. return url;
  7. }

对于catch语句,会创建一个新的变量对象,包含被抛出的错误对象的声明。

块级作用域

在ES6之前,ECMAScript 是没有块级作用域的,只能通过函数作用域来模拟块级作用域。
如果初始化变量时没有使用var声明,该变量将会自动添加到全局变量中(在严格模式下则会报错)。

查询标识符

在读取或写入时要引用一个标识符,需经过搜索来确定标识符实际代表什么。具体的搜索方法就是沿着作用域链向上搜索,直到全局环境。

垃圾收集

JavaScript 有自动垃圾收集机制。主要实现方式有标记清除和引用计数,现在主要使用前者。
在变量进入环境时,标记为“进入环境”。当变量离开环境时,标记为“离开环境。被标为”离开环境”的变量将在下一次垃圾收集中被回收。
尽管JavaScript 中不必操心内存管理问题,但手动解除引用仍然是优化内存占用的最佳方式。具体来说就是将其值设置为null 来释放引用。


CH05 引用类型

Object 类型

创建Object 实例的方式有两种。
1. 使用new 操作符调用Object 构造函数
2. 使用对象字面量
其中对象字面量是向函数传递大量可选参数的首选方法。

另外访问对象属性可使用点表示法和方括号表示法,其中后者可以使用变量来访问属性。

Array 类型

创建Array 实例的方法同样有使用构造函数和字面对象量两种。
数组中的项能存放任意类型的数据。
数组有一个length属性,总等于数组的长度。值得注意的是,length值是可写的。也就是说,可通过改写length值来增删数组中的项。

检测数据

  1. 使用value instanceof Array。缺点是跨执行环境时无法检测(iframe)
  2. ES5中的Array.isArray()
  3. 使用Object.prototype.toString.call(value),判断是否为”[object Array]”

栈方法

push 返回值:数组长度
pop 返回值:移除的项。

队列方法

shift 返回值:移除的项
unshift 返回值:数组长度

重排序方法

sort:默认按字典顺序排序,接受排序函数,修改原数组并返回排序后的数组
reverse:修改原数组并返回排序后的数组

操作方法

concat:拼接并返回新数组
slice:返回切片副本
splice:向数组中部插入项(同时可删除)

位置方法

indexOf(), lastIndexOf() (使用严格相等符)

迭代方法

every(), filter(), forEach(), map(), some()
这些方法都会跳过值为undefined 的项

归并方法

reduce(), reduceRight()

Date 类型

直接调用new Date() 默认获得当前时间。传入UNIX时间能得到对应的日期。传入能表示日期的字符串,内部会调用Date.parse(),解析出对应的日期。

日期格式化方法

toDateString(), toTimeString(), toUTCString()等

RegExp 类型

ECMAScript 通过RegExp 类型来支持正则表达式。

  1. var expression = / pattern / flags

其中flags有下面几个:
1. g: 全局模式,表示应用于所有字符串
2. i:表示不区分大小写
3. m:表示多行模式,到达一行文本末尾时还会继续查找下一行
4. y:执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标志。
5. u:使用unicode模式

RegExp 实例方法

exex(), test()

Function 类型

在ECMAScript 中,函数实际上以为是对象,他们都是Function 类型的实例。
使用function关键字声明函数时,解析器会对其进行函数声明提升(而函数表达式则不会提升)。

函数内部属性

函数内部有argumentsthis两个特殊的对象。前者保存着函数参数,后者是函数执行的环境对象的引用(在调用函数前,this的值不确定)。

函数属性和方法

每个函数都包含两个属性:lengthprototype。其中length是指函数期望接受的参数数目,而prototype自然就是其原型,保存着各种实例方法。

基本包装类型

每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装对象类型的对象,从而让我们能调用一些方法来操作这些数据。
大致过程为:
1. 创建对应的基本包装类型实例
2. 在实例上调用指定的方法
3. 销毁这个实例

需要注意的是,number 类型在调用方法时,需要连续使用两个点操作符,来错误解析为小数:

  1. 1.toString(); // SyntaxError
  2. 1..toString(); // "1"

Global 对象

实质上,没有全局变量或全局函数,所有在全局作用域中定义的变量和函数,都是Global 对象的属性。
此外它还有其它的一些方法:
1. URI编码方法:encodeURI, encodeURIComponent
2. eval方法:将字符串作为JavaScript代码执行
3. window 对象:浏览器中,Global对象以window对象的形式实现。(但除此之外还承担了很多别的任务)

Math 对象

提供数学中的特殊值和方法:
1. min()和max()方法
2. 舍入方法:Math.ceil(), Math.floor(), Math.round()
3. random() 方法


CH06 面向对象的程序设计

属性类型

数据属性

数据属性包含四个描述其行为的特性。(使用两对方括号包裹表示JavaScript 中不能直接访问它们)
1. [[Configurable]]: 表示是否能通过delete删除属性或修改为访问器属性。
2. [[Enumerable]]: 能否通过for-in循环返回
3. [[Writable]]: 能否修改属性值
4. [[Value]]: 这个属性的值

访问器属性

  1. [[Configurable]]: 表示是否能通过delete删除属性或修改为访问器属性。
  2. [[Enumerable]]: 能否通过for-in循环返回
  3. [[Get]]: 读取属性时调用的函数。
  4. [[Set]]: 写入属性时调用的函数。

数据属性和访问器属性都可以使用Object.defineProperty()来定义。

创建对象

创建单个对象

  1. //早期方法,现在很少用
  2. var person = new Object();
  3. person.name = "John";
  4. //对象字面量语法,直观
  5. var person = {
  6. name: "John"
  7. }

存在问题:当需要创建多个对象时,要写大量重复代码

创建多个对象

1.工厂模式
将创建对象的过程封装成一个函数

  1. function createPerson(name){
  2. var o = New Object();
  3. o.name = name
  4. return o;
  5. }
  6. var person1 = createPerson("John");
  7. var person2 = createPerson("Mike");

工厂模式的问题:创建的对象没有其类型的名字,所以无法判断两个对象是否为同一个类型

2.构造函数模式
为了解决对象类型的问题,可使用构造函数来创建对象,该方法的核心在于使用了new操作符
来看看对函数使用new操作符会发生什么

  1. 创建一个新的对象
  2. 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回创建的新对象

于是我们就可以把构造函数写成如下形式:

  1. function Person(name){
  2. this.name = name;
  3. this.sayName = function(){
  4. alert(this.name);
  5. }
  6. }
  7. var person1 = new Person("John");
  8. var person2 = new Person("Mike");

注意其实不用new操作符这个函数也能跑,只是this指针就指向了windows对象了;为了表明这是我们定义的一种对象类型,函数名的首字母要大写(毕竟关键字都是function,容易和一般的函数混淆)
这样,person1和person2分别保存着Person的一个不同的实例,而且两个对象都有一个constructor属性,该属性指向构造函数Person,如下所示:

  1. alert(person1.constructor == Person);//true
  2. alert(person2.constructor == Person);//true

构造函数的问题:

直接看例子

  1. console.log(person1.sayName == person2.sayName);//false

可以看到,用上述方法定义的方法是两个不同的对象实例

  1. this.sayName = New Function(console.log(this.name));//这种写法等价于上述写法

然而这个方法的功能是一样的,创建不同的实例是没有必要的。所以调用方法时,其实只要找到对应的函数即可,而不是调用时创建一个函数。那么我们其实可以把函数写在构造函数外面:

  1. function Person(name){
  2. //...
  3. this.sayName = sayName;
  4. }
  5. function sayName(){
  6. console.log(this.Name);
  7. }

新的问题来了:这些定义在全局作用域的函数只能被某些对象使用,似乎失去了全局作用域的意义。另一方面,各种构造函数的方法都混杂着定义在全局当中,对构造函数来说毫无封装性可言。

3.原型模式
每一个函数都有prototype属性,属性值是指向其原型对象的指针(对应的原型对象有constructor属性指向构造函数)。该原型对象的属性和方法由所有实例共享。
所以推荐把方法定义在原型上
注意:如使用对象字面量改写原型:

  1. Person.prototype = {
  2. this.name = name;
  3. }

实质上变更了Person.prototype指针的指向,而不是在原来的基础上修改,因此也丢失了constructpr属性。

4.组合使用构造函数模式和原型模式(推荐)
综上,创建自定义类型时,建议方法定义在原型上,属性定义在构造函数当中。
加强版(定义在原型上的方法也写进构造函数中):

  1. function Person(name, age, job){
  2. //属性
  3. this.name = name;
  4. this.age = age;
  5. this.job = job;
  6. //方法
  7. //含多个方法时也只需要一条if语句,因为能通过表示初始化成功
  8. if( type of this.sayName != 'function'){
  9. Person.prototype.sayName = function(){
  10. alert(this.name);
  11. }
  12. }
  13. }

其它构造函数模式

5.寄生构造函数模式(其实是工厂模式,但可以用new
试想我们要构建一个具有特殊功能的数组对象,要怎么做?

不建议修改原生对象的原型。在不同环境下可能发生命名冲突;还有可能意外重写原生方法。

  1. function specialArray(){
  2. //创建数组
  3. var values=new Array();
  4. //添加值
  5. //注意这里用apply是为了把arguments这个伪数组拆开,否则等于把arguments这个对象整体作为values数组的一项
  6. //ES6中可以用拓展运算符代替,更直观,即values.push(...arguments);
  7. values.push.apply(values,arguments);
  8. //添加方法
  9. values.toPipedstring=function(){
  10. return this.join("|");
  11. }
  12. //返回数组
  13. return values;
  14. }
  15. var colors=new specialArray('red','yellow','green');
  16. alert(colors.toPipedstring()); // red|yellow|green

其实就是可用new的工厂模式,工厂模式的缺点它都有,用instanceof运算符也只能返回Object,尽量不建议使用这种模式;

6.稳妥构建函数模式(安全)
稳妥对象:即无公共属性,同时其方法不引用this的对象。
即传入参数直接用于构建方法,不在属性上停留。如:

  1. function Person(name){
  2. var o = new Object();
  3. 0.sayName = function(){
  4. alert(name);
  5. };
  6. return o;
  7. }

继承

原型链

试想一下,如果用原型A的实例来作为构造函数B的原型会发生什么?
没错,就是实例B拥有了原型A的属性和方法。
因为这涉及到指针操作,所以要注意赋值操作的顺序

只用原型链继承存在的问题

  1. 因为原型A的实例是构造函数B的原型,所以原型A的实例的引用类型属性会被所有实例B所共享(还是那个道理,不想被共享的引用类型不要放原型中)
  2. 无法在不影响所有实例的前提下向超类型(即原型A)的构造函数传递参数

实现继承的方法

1.结构构造函数(相当于构造函数代码拼接)
直接看代码吧:

  1. function SuperType(name){
  2. this.name = name;
  3. }
  4. function SubType(){
  5. //把this传递过去相当于把上面的代码复制了过来
  6. SuperType.call(this, "Nicholas");
  7. //实例属性
  8. this.age = 29;
  9. }

存在的问题

还是构造函数的老问题,方法不共用

2.组合继承(ES5最常用)
即属性用构造函数继承,方法通过原型链继承

  1. //超类型方法的定义
  2. SuperType.prototype.sayName = function(){
  3. alert(this.name);
  4. }
  5. //子类型继承属性
  6. function subType(name, age){
  7. SuperType.call(this, name);
  8. this.age = age;
  9. }
  10. //子类型继承方法
  11. SubType.prototype = new SuperType();
  12. //子类型定义方法
  13. SubType.prototype.sayAge = alert(this.age);

3.原型式继承(即对象的浅复制)
思想是用一个实例函数作为另一个对象的原型。
ES5中提供了方法Object.create()来实现,第一个参数为用作原型的实例对象,第二个参数(可忽略)为额外的属性值,需要同时写上属性描述符;

4.寄生式继承
即用函数把定义自己的方法这一操作打包起来,同样方法不能共享。

5.寄生组合式继承(最优选择)
细心的话可以发现,上述的组合继承中在原型中继承的属性是多余的
实现方法如下:

  1. function inheritPrototype(subType, SuperType){
  2. //浅复制超类型的原型(即只含有方法)
  3. var prototype = Object.create(SuperType.prototype);
  4. //将刚才浅复制的原型的constructor指向subType
  5. prototype.constructor = subType;
  6. //subType的prototype指向上面复制的原型
  7. subType.prototype = prototype;
  8. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注