首先看看下面两个"1+1=2"的问题:json
var arr = [1]; arr.length = 3; alert(arr); // [1, undefined, undefined]
var outter = "sunshine"; function showScope() { var inner = "darkness"; console.log(outter); //"sunshine" } console.log(typeof inner) // undefined
好了,接下来进入正文。segmentfault
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;dom
Enumerable: 可否经过for-in循环遍历属性,默认为true;函数
Writable: 可否修改属性的值;this
Value: 存放属性的值,默认为 undefined;spa
Configurable: 同上;prototype
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中的闭包
上文谈到的都是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。