15分钟了解JS面向对象精要

本文是我读完《JavaScript面向对象精要》后写的学习笔记。这本小册子总共还不到一百页,但麻雀虽小,五脏俱全,可以称得上是微言大义,不愧对精要二字。我反复读了三四遍,总结出了以下的知识点。

第一章 原始类型和引用类型

一、两种类型

  1. 原始类型

    通过 var 赋值给一个变量时,原始类型的值直接保存在变量中。

    • 布尔 (boolean)
    • 数字 (number)
    • 字符串 (string)
    • 空类型 (null)
    • 未定义 (undefined)

  2. 引用类型

    通过var赋值给一个变量时,变量并不包含该引用类型的值,而是保存了一个指向该引用类型的对象(实例)的指针。js提供了以下这些内建引用类型。)

    • Array (数组)
    • Date (日期和时间)
    • Error (错误)
    • Function (函数)
    • Object (通用对象)
    • RegExp (正则表达式)

二、鉴别类型

  1. 鉴别原始类型

    • 布尔、数字、字符串、未定义这四个原始类型直接用typeof鉴别,分别返回“boolean”、“number”、”string”、“undefined”。
    • 空类型需要用三等号与null进行比较 (value === null),返回true就是空类型,false则不是。
  2. 鉴别引用类型

    • 鉴别函数直接用typeof,是函数则返回 function
    • typeof鉴别非函数的引用类型时,都返回object,无法精确鉴别,所以需要用instanceof来鉴别。(注意:所有引用类型的instanceof Object 都返回true,因为instanceof可以用来鉴别继承类型,所有引用类型都继承自Object。)
    • 鉴别数组还可以直接用Array.isArray("被鉴别的对象")来实现,这个方法可以解决不同全局上下文的问题。

三、原始封装类型

这一章的难点在于理解“原始封装类型”,首先要知道,原始类型不是对象,JavaScript为了让原始类型看上去像对象一样(提供一致性体验),做了以下几件事:

  1. 设置了原始封装类型

    • 原始封装类型共有三种:分别是String, Number 和 Boolean,它们都属于“特殊引用类型”。
    • 原始封装类型具有方法,比如String的toLowerCase()、Number的toFixed()、Boolean的toString(),String的方法最多,也最常用。
    • 当读取字符串、数字或布尔值这三种原始类型时,相应的原始封装类型会被自动创建,并创建出一个原始封装类型的实例(对象),在这个实例上自然可以使用相应的原始封装类型的方法。使用结束后,JavaScript会自动销毁这个实例(自动打包)。
  2. 原始方法

    • 原始封装类型的创建、生成实例、销毁实例都是在后台执行的,所以表面上看好像是原始类型变成了对象一样可以使用方法。
    • 注意:null和undefined没有方法。

原书的代码例子很好的解释了这个概念:

1
2
3
4
var name = "Nicholas"
var firstChar = name.charAt(0);
console.log(firstChar);    /* 输出 "N" */

上面是我们看到的结果,而背后发生的事如下:

1
2
3
4
5
6
var name = "Nicholas"  /* 这时候就自动创建了原始封装类型String */
var temp = new String(name);  /* 创建原始封装类型String的实例temp */
var firstChar = temp.charAt(0);  /* 读取原始封装类型的方法,赋值给firstChar */
temp = null;  /* 任务完成,销毁实例 */
console.log(firstChar);  /* 输出 "N" */

第二章 函数

一、函数是什么

在JavaScript里,函数就是对象,也就是一个引用类型Function的一个实例。让函数区别于其他对象的是函数自己的内部属性[[Call]],typeof对含有[[Call]]的对象返回Function。除此之外,函数使用起来和其他对象没有区别,函数就是值。

函数具有两种字面形式,分别为:

  • 函数声明:
1
2
3
4
function add(num1, numb2) {
    return num1 + num2;
}

  • 函数表达式
1
2
3
4
var add = function(num1, num2) {
    return num1 + num2;
);

区别:

  1. 函数声明会被提升至上下文的顶部。
  2. 函数表达式最后有一个分号。

二、函数的参数

这里有两对概念一定要区分:

  1. 形参和实参
  2. function.length 和 arguments.length

首先,形参用书中的话来讲就是“函数期望的参数”,也就是声明函数时候写入的参数,形参被保存在函数的length属性中。

而实参,顾名思义,就是真正传入函数的参数,实参被保存在一个叫arguments的对象中。arguments对象具有两个类似数组的方法:

  • arguments.length返回arguments值的个数;
  • arguments[number]返回相应位置的值,类似数组的索引。

注意: arguments对象不是数组。

当实参大于形参的时候,多余的实参不起作用;当形参大于实参的时候,多余的形参会返回undefined

三、this对象

this出现在全局作用域的时候,代表全局对象,即浏览器里的window。this出现在函数作用域里的时候,代表调用该函数的对象。

改变this值的三种方法:

  1. call()方法
  2. apply()方法
  3. bind()方法

call()方法第一个参数指定一个新的this值,后面的参数是需要被传入的参数。例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sayNameFaoAll(label) {
    console.log(label + ":" + this.name);
}

var person1 = {
    name: "Li Bai"
};

var person2 = {
    name: "Du Fu"
};

var name = "Shi Ren";

//这一步相当于直接sayNameForAll("global");
sayNameForAll.call(this, "global");  /* 输出 "global: Shi Ren" */

//将this指向person1,并传入参数“person1”
sayNameForAll.call(person1, "person1");  /* 输出 "person1: Li Bai" */

//将this指向person2,并传入参数“person2”
sayNameForAll.call(person2, "person2");  /* 输出 "person2: Du Fu" */

apply()和call()的工作方式一样,唯一区别是apply()只接受两个参数,第二个参数可以是数组对象或者arguments对象。

bind()方法是ES5中新加的方法。与之前两个方法的用法基本相似,需要注意的是bind()返回的是一个新函数,通常赋值给一个变量,而call()和appy()则是直接引用原函数。例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function sayNameForAll(label){
    console.log(label + ":" + this.name);
}

var person1 = {
    name: "Li Bai"
};

//这里赋值给一个新变量
var sayNameForPerson1 = sayNameForAll.bind(person1);  

//这里执行新函数
sayNameForPerson1("person1");  /* 输出 "person1:Li Bai" */

第三章 理解对象

定义对象的属性:

  • 第一次定义时,JavaScript在对象上调用内部方法 [[Put]]
  • 赋予属性一个新值时,JavaScript调用内部方法 [[Set]]

检测对象是否含有某个属性:

  • in 操作符可以查找给定名称的属性,但不区分自有属性和原型属性,如果找到则返回true

    1
    2
    
    console.log("property" in object); /* 返回 true 或 false */
    
  • 所有对象都拥有hasWenProperty()方法,只检测是否存在自有属性

    1
    2
    
    console.log(object.hasWenProperty("property")); /* 返回 true 或 false */
    

删除对象的某个属性用 delete 操作符,会调用[[Delete]]内部方法

1
2
delete property.object  /* 删除成功返回 true, 失败返回 false */

属性的特征[[Configurable]]规定了属性是否可以配置,默认是可以配置的。可以通过Object.defineProperty()方法改变:

1
2
3
4
Object.defineProperty(object, "property", {
   configurable: false   /* 变为不可配置,属性描述对象与属性特征同名,不带中括号,首字母小写。 */
})

属性枚举(遍历),可以用以下两种方法

  • for (property in object)
  • 或者ES5引入的 Object.keys()方法,区别是这个方法可以返回属性名的数组
  • 属性的特征[[Enumerable]]规定了属性是否可以遍历,默认是可以枚举的。可以通过Object.defineProperty()方法改变:
1
2
3
4
Object.defineProperty(object, "property", {
    enumerable: false  //变为不可枚举,属性描述对象与属性特征同名,不带中括号,首字母小写。
})

注意:当使用Object.defineProperty()时,一定要给所有属性特征制定一个值,否则会被默认制定为false。

用Object.defineProperties()可以为一个对象同时定义多个属性。

获取对象的属性特征可以用 Object.getOwnPropertyDescription(object, “property”),会返回一个属性描述对象,包含四个属性(两个通用,两个特殊)

对象的属性类型有两种:

  1. 数据属性

    • 数据属性包含一个值
    • 拥有通用特征[[Enumerable]]和[[Configurable]]
    • 拥有独特的属性特征 [[Value]]和[[Writable]]
      • 属性的值保存在[[Value]]中
      • [[Writable]]是个布尔值,表示该属性是否可以写入
  2. 访问器属性

    • 访问器属性不包含值,而是定义了一个getter和一个setter。访问器属性只需要getter和setter的任意一个,也可以都有。
    • 拥有通用特征[[Enumerable]]和[[Configurable]]
    • 拥有独特的属性特征[[Get]]和[[Set]]

禁止修改对象的方法

  1. 禁止扩展
    • 使对象无法添加新属性
    • 代码是:Object.preventExtensions(object);
    • 用Object.isExtensible(object)来检测是否可以扩展
  2. 封印对象
    • 使对象无法添加新属性、无法删除属性或更改其类型,只能读写。(禁止扩展 + 不可配置)
    • 代码是: Object.seal(object);
    • 用Object.isSealed(object)来检测是否封印了
  3. 冻结对象
    • 使对象无法添加新属性、不能删除属性或更改类型、不能给属性赋值。(禁止扩展 + 不可配置 + 不能读写)
    • 代码是:Object.freeze(object);
    • 用Object.isFrozen(object)来检测是否冻结了

第四章 构造函数和原型对象

一、构造函数

构造函数就是用new创建对象时调用的函数

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Person(name) {   /* 构造函数首字母大写 */
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    };
}

//始终确保用new来调用构造函数
var person1 = new Person1("Li Bai");
var person2 = new Person2("Du Fu");

console.log(person1.name);   /* "Li Bai" */ 
console.log(person2.name);   /* "Du Fu" */ 

person1.sayName();  /* 输出 "Li Bai" */   
person2.sayName();  /* 输出 "Du Fu */ 

使用构造函数的目的是为了轻松创建许多拥有相同属性和方法的对象

二、原型对象

可以把原型对象看作是对象的基类

构造函数创建出的新对象会拥有构造函数的原型对象。

可以用Object.getPrototypeOf(object)方法读取[[Prototype]]属性的值,可以用object.isPrototypeOf(object)方法检查第一个object是否是第二个prototype的原型对象。

重要:无法给原型对象的属性赋值,会变成创建一个同名自有属性。

可以给构造函数中的原型对象创建新的属性,与在构造函数中创建新属性的区别是,调用原型对象上的新属性时,this值指向用new新生成的对象,而不是指向构造函数。

可以用对象字面形式改写原型对象,但要注意手动把constructor属性(原型对象特有的属性)指向构造函数。(constructor属性是原型对象特有的属性,默认会指向Object)

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function Person(name) {
    this.name = name;
}

Person.prototype = {
    constructor: Person,
    
    sayName: function() {
        console.log(this.name);
    },
    
    toString: function() {
        return "[Person " + this.name + "]";
    }
};

重要: 构造函数和对象实例之间没有直接联系,他们通过原型对象相连

[[prototype]]属性是指向原型对象的指针。对原型对象的所有改变可以无缝立即生成为对象实例的属性,无论何时改变都可以,因为JavaScript查找对象属性每次都会先查找自有属性,然后再查找原型对象的属性。

Object.freeeze和Object.seal只对自有属性生效,完全可以再改变原型对象的属性。

当然,理论上可以改变内建对象的原型对象,比如给Array, String, Boolean的原型对象赋新值,但不推荐这么做。

第五章 继承

JavaScript内建的继承方法被称为原型对象链,也叫原型对象继承

所有的对象都继承自Object.prototype,也就是说Object.prototype是原型对象链的最顶端,它自己的[[prototype]]属性值为null。

Object.prototype具有五种方法,其他对象都可以使用:

  • hasOwnProperty()
  • propertyIsEnumerable()
  • isPrototypeOf()
  • valueOf()
  • toString()

对象继承

使用var 生成一个对象的时候,JavaScript会隐式指定Object.prototype为对象的[[prototype]]。可以用Object.create()方法显式指定,例子:

 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
27
28
29
30
31
32
33
34
35
var person1 = {
    name: "Li Bai",
    occupation: "Shi Ren"
};

var person1 = Object.create(Object.prototype, {
               name: {
                   configurable: true,
                   enumerable: true,
                   value: "Li Bai",
                   writable: true
               }
               
               occupation: {
                   configurable: true,
                   enumerable: true,
                   value: "Shi Ren",
                   writable: true
               }

          });

var person2 = Object.create(person1, {
               //这里新创建自由属性name,会替换继承自person1的name属性
               name: {  
                   configurable: true,
                   enumerable: true,
                   value: "Du Fu",
                   writable: true
                   }
              });

person1.occupation();    /* 输出“Shi Ren” */
person2.occupation();    /* 输出“Shi Ren” */

构造函数继承

首先要明白,创建构造函数时,JavaScript在背后隐式的做了这些事:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//表面上声明构造函数:
function Shiren() {
    work1: "Xie Shi";
    work2: "Xie Ci";
}

//背后逻辑:
Shiren.prototype = Object.create(Object.prototype, {
                       //constructor是自有属性,指向自身
                       constructor: {
                           configurable: true,
                           enumerable: true,
                           value: Shiren,
                           writable: true
                       }
                    });

构造函数继承

方法是让新构造函数的原型对象变成被继承的构造函数的一个新实例

例子:

 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
27
28
29
30
31
32
33
34
35
36
37
38
function Juxing(chang, kuan) {
    this.chang = chang;
    this.kuan = kuan;
}

Juxing.prototype.mianJi = function() {
    return this.chang * this.kuan;
}

function ZhengFangXing(bian) {
    this.chang = bian;
    this.kuan = bian;
}

//10-13行的代码可以用“构造函数窃取”实现,这种方法也叫“伪类继承”
function ZhengFangXing(bian) {
    Juxing.call(this, bian, bian);
}

ZhengFangXing.prototype = new Juxing();
ZhengFangXing.prototype.constructor = ZhengFangXing;

//20、21两行代码可以被替换为如下
ZhengFangXing.prototype = Object.create(Juxing.prototype, {
                       constructor: {
                           configurable: true,
                           enumerable: true,
                           value: ZhengFangXing,
                           writable: true
                       }
                    });

var ju = new Juxing(5, 10);
var zheng = new ZhengFangXing(6);

console.log(ju.mianJi());    /* 50 */
console.log(zheng.mianJi());   /* 36 */  

第六章 对象模式

如何让对象的属性私有?这一章简要介绍了几种方法

  1. 模块模式

    模块模式是一种用于创建拥有私有数据的单件对象的模式

    基本做法: 使用立调函数表达(IIFE)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    var yourObject = (function() {
        
          //私有数据变量,无法在外部访问
          
      return {
          
          //公共方法和属性,可以访问自己的私有数据
              
      }
        
    }());
    
  2. 构造函数的私有成员

    用在同样需要私有属性的自定义类型上

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    function Person(name) {  
        var age = 25;  /* 本地变量 */
            
        this.name = name;
            
        this.getAge = function() {           
            return age;            
        };
            
        this.growOlder = function(){            
            age++;  
        };
      }
    

另一种伪继承方法:混入(mixin)

函数mixin()接受两个参数:接受者 和 提供者。 该函数的目的是将提供者所有可枚举属性复制给接收者

例子:

1
2
3
4
5
6
7
8
var person = mixin( new Person(), {
    chaoDai: "Tang Chao",
    
    sayChaoDai: function() {
        console.log(this.name);      
    } 
});

这个例子表达的意思是:构造函数Person()的新实例person,混入了新的属性(chaoDai 和 sayChaoDai),而没有改变person的原型对象链。

注意:提供者的访问器属性会自动变成接受者的数据属性

作用域安全的构造函数

构造函数生成新实例如果不用 new, this值会强制指向全局对象, 严格模式下会抛出错误。可以通过以下方法来制造作用域安全的构造函数(也就是说用不用new都可以的构造函数)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function Person(name) {

    if (this instanceOf Person) {
        this.name = name;  /* 当自己是被new调用时,正常设置name属性 */
    } else {
        return new Person(name);  /* 当不是被new调用时,则以new递归调用自己,以确保创建正确的实例 */
    }
}

var person1 = new Person("Li Bai");
var person2 = Person("Du Fu");

console.log(person1 instanceof Person); /* true */ 
console.log(person2 instanceof Person); /* true */