首先看看下面两个"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。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。