首先看看下面两个"1+1=2"的问题:

问题一:为什么改变length的值,数组的内容会变化?

var arr = [1];
arr.length = 3;
alert(arr);   // [1, undefined, undefined]

问题二:为什么在showScope函数内能访问outter,在函数外不能访问inner?

var outter = "sunshine";

function showScope() {
    var inner = "darkness";
    
    console.log(outter);    //"sunshine"
}

console.log(typeof inner)   // undefined

好了,接下来进入正文。

一、对象的属性

var person = {
    name: "Simon",
    _age: 21,
    isYoung: true,
    friends: ["Johnny", "Carlton", "Amy"],
    sayName: function() {
        console.log(this.name);
    }
    educate: {
        primarySch: "",
        highSch: "",
        university: ""
    }
};

上面的person对象是JS对象的字面量形式,本质上是一个键值对的无序集合,这些键值 对叫做属性。属性的名称只能是字符串形式的,而属性的值可以是字符串、数字、布尔值等基本类型,也可以是数组、函数、对象等引用类型。值得一提的是,如果属性的名称是JS能够识别的标识符,如name、first_name、$name,则在定义属性时不用像json那样为属性名加上引号;但属性名称是first-name这种JS无法识别的标识符时,就需要为其加上引号了。这两种情况也会造成访问方式不同,前者既可以通过person.first_name的形式访问,也可以通过person[first_name]的形式访问。但后者只能通过中括号的形式访问。

如果要对属性分类的话,属性可以分为两类:数据属性、访问器属性。这两种属性都分别有着一些特性:

数据属性
  • Configurable: 能否修改或删除属性,默认为true;

  • Enumerable: 能否通过for-in循环遍历属性,默认为true;

  • Writable: 能否修改属性的;

  • Value: 存放属性的值,默认为 undefined;

访问器属性
  • Configurable: 同上;

  • Enumerable: 同上;

  • Get: 在读取属性的值时调用的函数;

  • Set: 在设置属性的值时调用的函数;

这些特性无法直接访问,但可以通过Object.defineProperty(obj, attr, descriptor)函数定义这些特性。
基于上面的person对象各举一个例子:

// 数据属性
Object.defineProperty(person, "name", {
    configurable: false
})

console.log(person,name); // Simon
person.name = "zai";
console.log(person,name); // Simon

//访问器属性
Object.defineProperty(person, "age", {
    get: function() {
        return this._age;
    },
    set: function(newValue) {
    
        if (newValue > 30) {
            this._age = newValue;
            this.isYoung = false;
        }
    }
})

到这里第一个问题就得到了解决,数组的length属性其实就是一种访问器属性。

此外操作属性的方法还有:Object.defineProperties 用来一次定义多个属性,Object.getOwnPropertyDescriptor(obj, attr) 用来读取属性的特性。另外可以通过delete操作符去删除Configurable值为true的属性。

二、如何创建对象

仅仅通过字面量的方式去创建对象显然是不现实的,因为当我们需要创建多个相似的对象时,这样做会产生大量的重复代码。需要一种科学的方式去创建对象。

function Person(name, age, friends) {
    this.name = name;
    this.age = age;
    this.friends = friends;
    // this.prototype = { constructor: this };
}

Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name);
    }
}

Person.prototype.sayAge = function() {
    console.log(this.age);
};

var simon = new Person("Simon", 22, ["Amy", "Johnny", "Carlton"]);
simon.sayName();   //委托

上面的代码结合了构造函数和原型两种方式去创建对象,首先聊聊构造函数:

构造函数

构造函数本质上还是函数,只不过为了区分将其首字母大写了而已。注意注释掉的代码是自动执行的,但这并不是构造函数独有的,每个函数在声明时都会自动生成prototype。构造函数不一样的地方在于它的调用方式——new,new调用构造函数的大致过程:

  • 产生一个新对象;

  • 将构造函数的作用域赋给新对象;

  • 执行构造函数中的代码;

  • 返回新对象或者指定返回的对象;

构造函数本质上仍是函数,所以当然可以直接调用,这样构造函数中的this就指的是全局对象,显然不符合预期。

原型

《JavaScript高级程序设计》上的一幅图很好的解释了原型、构造函数、实例之间的关系:
图片描述

执行simon.sayName( )时,首先在simon对象本身的作用域中寻找sayName,没有找到之后再去其原型Person.prototype中寻找,这个过程叫做委托。那么问题就来了,当我们不知道一个对象的构成时,如何去判断一个属性属于对象还是其原型呢?obj.hasOwnProperty(propName)就是做这个事情的函数,常常被用在for-in循环遍历对象的属性的过程中,与for-in类似的两个方法:Object.keys(obj)、Object.getOwnPropertyNames(obj) 这两个方法返回的都是属性名的数组,都不包括原型中的属性,区别在于前者和for-in一样只遍历enumrable为 true的属性,而后者遍历所有属性。

三、继承

这里给出一种JavaScript实现继承的方式:

function Vehicle(maxSpeed, wheels) {
    this.maxSpeed = maxSpeed;
    this.wheels = wheels;
}

Vehicle.prototype.checkMaxSpeed = function() {
    console.log(this.maxSpeed);
};

function Car(brand, maxSpeed) {
    Vehicle.call(this, maxSpeed, 4);
    this.brand = brand;
}

Car.prototype = new Vehicle();
Car.prototype.constructor = Car;
Car.prototype.checkBrand = function() {
    console.log(this.brand);
};

var panemera = new Car("Panemera", 250);

这里的关键在于在Car中调用Vehicle,向父类构造器传递参数,初始化子类的属性,再进行扩充(brand),当然仅仅有构造函数还是不行的,还需要原型链才能更好地实现继承,这里Car的原型是Vehicle的一个实例,值得注意的是Car.prototype = new Vehicle();之后,原本的constructor丢失了,新的constructor在这里指向了Vehicle,需要重置为Car。

之前提出的第二个问题其实就是用继承来实现的:

function showScope() {
    // scope代表当前作用域
    var oldScope = scope;
    var Scope = function() {};
    
    //继承当前作用域
    Scope.prototype = scope;
    scope = new Scope();
    
    // 进入函数作用域,扩充作用域
    advance("{");
    parse(scope);     // 用当前作用域做解析
    advance("}");
   
   scope =oldScope;
}

假设showScope是解析作用域的函数,它的实现机制大概是:进入函数作用域之前保存当前作用域,新建一个继承了当前作用域的对象并用它取代当前作用域,解析左括号进入函数作用域并对当前作用域进行扩充,使用扩充后的作用域进行解析,解析右括号离开函数作用域,恢复进入函数前的作用域。

四、私有成员的实现

最后说说JavaScript中私有成员的实现,一个很有趣的例子:

function AladdinLamp() {
    var limit = 3;
    
    function rubLamp() {
        if (limit > 0) {
            limit -= 1;
            return true;
        } else {
            return false;
        }
    }
    
    this.satisfyWish = function() {
        return rubLamp() ? Math.random() : null;
    };
}

这里的limit和rubLamp都是AladdinLamp的私有成员,无法从外部直接访问,只能通过唯一暴露出来的satisfyWish调用,这实际上是一种闭包,关于闭包请参考本专栏中的浅谈JavaScript中的闭包

五、ES6中的类与继承

上文谈到的都是ES5,那么ES6有什么不同呢,先来看看ES6中的类:

class Vehicle {
    constructor(maxSpeed, wheels) {
        this.maxSpeed = maxSpeed;
        this.wheels = wheels;
    }
    
    checkMaxSpeed() {
        console.log(this.maxSpeed);
    }
    
    static openDoor() {
        console.log("Welcome");
    }
}

Vehicle.length = 100;

let bike = new Vehicle(40, 2);
// TypeError
bike.openDoor();

不同之处在于构造函数换成了Class,其实Class本质上也是函数,constructor就相当于ES5中的构造函数,而直接在类中声明的checkMaxSpeed实际相当于 Vehicle.prototype.checkMaxSpeed = ...
有意思的是ES6中多了静态方法的实现,这里的openDoor无法在实例中调用,可以通过Vehicle.openDoor直接调用,可以继承给子类。另外通过Vehicle.props = ...的形式可以定义静态变量。最后注意Vehicle只能通过new调用,否则会报错,是因为在constructor中检测了new.target。
再看看ES6中的继承:

class Car extends Vehicle {
    constructor(maxSpeed, wheels, brand) {
        super(maxSpeed, wheels);
        this.brand = brand;
    }
    
    checkBrand() {
        console.log(this.brand);
    }
}

继承的关键在于constructor中调用了super,即父类的构造函数。这里一定要调用super,因为子类的this是由super创建的,之后再去扩充this。


simon_z
254 声望12 粉丝