理解Javascript的动态语言特性

时间:2024-10-23 18:03:32

原文:理解Javascript的动态语言特性

理解Javascript的动态语言特性

Javascript是一种解释性语言,而并非编译性,它不能编译成二进制文件。

理解动态执行与闭包的概念

动态执行:javascript提供eval()函数,用于动态解释一段文本,并在当前上下文环境中执行。

首先我们需要理解的是eval()方法它有全局闭包和当前函数的闭包,比如如下代码,大家认为会输出什么呢?

var i = 100;
function myFunc() {
var i = 'test';
eval('i = "hello."');
}
myFunc();
alert(i); //

首先我们来看看先定义一个变量i=100,然后调用myFunc这个函数,然后修改局部变量i,使他从值 ’test’变成’hello’, 但是我们知道eval的含义是立即执行一段文本的含义;因此上面的代码我们可以写成如下代码:

var i = 100;
function myFunc() {
var i = 'test';
(function(){
return (i = "hello.");
})();
}
myFunc();
alert(i); //

这样就很明显了,执行myFunc()这个方法后,i的值从test变为hello的值,但是由于是闭包,i的值为hello,它不能被外部使用,所以浏览器打印的都是100值;

我们都知道eval()是javascript的全局对象Global提供的方法,而如果要访问Global对象的方法,可以通过宿主对象-在浏览器中是window来提供;按道理来说,下面的代码应该也是输出100;如下:

var i = 100;
function myFunc() {
var i = 'test';
window.eval('i="hello."');
}
myFunc();
alert(i);

然后不幸的是:在IE下不管是window.eval()还是eval()方法输出的都是100;但是在标准浏览器下使用window.eval(),输出的是hello,使用eval()方法的输出的是100; 因为IE下使用的是JScript引擎的,而标准浏览器下是SpiderMonkey Javascript引擎的,正是因为不同的javascript引擎对eval()所使用的闭包环境的理解并不相同。

理解eval使用全局闭包的场合

如下代码:

var i = 100;
function myFunc() {
var i = 'test';
window.eval('i="hello."');
}
myFunc();
alert(i);

在标准浏览器下,打印的是hello,但是在IE下打印的是100;如果使用如下代码:

var i = 100;
function myFunc() {
var i = 'test';
//window.eval('i="hello."');
eval.call(window,'i="hello"');
}
myFunc();
alert(i);

也是一样的,也是给eval方法提供一种访问全局闭包的能力;但是在IE下Jscript的eval()没有这种能力,IE下一只打印的是100;不过在IE下可以使用另一种方法得到一个完美的结果,window.execScript()方法中执行的代码总是会在全局闭包中执行,如下代码:

var i = 100;
function myFunc() {
var i = 'test';
window.execScript('i="hello."');
//eval.call(window,'i="hello"');
}
myFunc();
alert(i); // 打印hello

JScript()引擎使用execScript()来将eval在全局闭包与函数闭包的不同表现分离出来,而Mozilla的javascript引擎则使用eval()函数的不同调用形式来区分它们。二者实现方法有不同,但是可以使用不同的方式实现全局闭包;

理解eval()使用当前函数的闭包

一般情况下,eval()总是使用当前函数的闭包,如下代码:

var i = 100;
function myFunc() {
var i = 'test';
eval('i="hello."');
}
myFunc();
alert(i); //

如上代码:因为eval作用与是函数内的代码,所以输出的是全局变量i等于100;

eval()总是被执行的代码文本视为一个代码块,代码块中包含的是语句,复合语句或语句组。

我们可以使用如下代码取得字符串,数字和布尔值;

eval('true'); // true

eval('"this is a char"');  // string

eval('3');  // 数字3

但是我们不能使用同样的方法取得一个对象;如下代码:

eval('{name:"MyName",value:1}');

如上代码会报错;如下:Uncaught SyntaxError: Unexpected

其实如上那样写代码,{name:"MyName",value:1},eval会将一对大括号视为一个复合语句来标识,如下分析:

  1. 第一个冒号成了 “标签声明”标示符。
  2. {“标签声明”的左操作数}name成了标签。
  3. MyName成了字符串直接量;
  4. Value成了变量标示符。
  5. 对第二个冒号不能合理地作语法分析,出现语法分析期异常;

如果我们只有这样一个就不会报错了,如下代码:

eval('{name:"MyName"}')

输出"MyName";

那如果我们想要解决上面的问题要如何解决呢?我们可以加一个小括号包围起来,使其成为一个表达式语句,如下代码:

eval('({name:"MyName",value:1})')

输出一个对象Object {name: "MyName", value: 1}

但是如下的匿名函数加小括号括起来在IE下的就不行了,如下代码:

var func = eval('(function(){})');

alert(typeof func); // IE下是undefined

在标准浏览器chrome和firefox是打印function,但是在IE下的JScript引擎下打印的是undefined,在这种情况下,我们可以通过具名函数来实现;如下:

eval('function func(){}');

alert(typeof func); // 打印是function

我们使用eval时候,最常见的是ajax请求服务器端返回一个字符串的格式的数据,我们需要把字符串的格式的数据转换为json格式;如下代码:

// 比如服务器返回的数据是如下字符串,想转换成json对象如下:

var data = '{"name":"Mike","sex":"女","age":"29"}';

console.log(eval("("+data+")"));

打印Object {name: "Mike", sex: "女", age: "29"} 就变成了一个对象;

// 或者直接如下 ,都可以

console.log(eval("("+'{"name":"Mike","sex":"女","age":"29"}'+")"));

我们还需要明白的是使用eval或者with语句,他们都会改变作用域的问题,比如使用eval如下代码:

var i = 100;
function myFunc(name) {
console.log('value is:'+i); //
eval(name);
console.log('value is:'+i); //
}
myFunc('var i = 10;');

如上代码,第一次执行的是100,第二次调用eval()方法,使作用域变成函数内部了,因此i变成10了;

理解动态方法调用(callapply)

Javascript有三种执行体,一种是eval()函数入口参数中指定的字符串,该字符串总是被作为当前函数上下文中的语句来执行,第二种是new Function(){}中传入的字符串,该字符串总是被作为一个全局的,匿名函数闭包中的语句行被执行;第三种情况执行体就是一个函数,可以通过函数调用运算符”()”来执行;除了以上三种之外,我们现在还可以使用call()方法或者apply()方法作为动态方法来执行;如下代码:

function foo(name){
alert("hi:"+name);
}
foo.call(null,'longen'); // 调用打印 hi: longen
foo.apply(null,['tugenhua']); // 调用打印 hi:tugenhua

call()方法与apply()方法 使用效果是一样的,都是调用函数,只是第二个参数不一样,apply第二个参数是一个数组或者arguments;

callapply中理解this的引用

如果我们将一个普通的函数将作为一个对象的方法调用的话,比如我现在有一个普通的函数如下代码:

function foo(){
alert(this.name);
}

现在我们下面定义2个对象如下:

var obj1 = {name:'obj1'};

var obj2 = new Object();

obj2.name = 'obj2';

那么现在我们使用这2个对象来分别调用哪个上面的普通函数foo;如下:

foo.call(obj1);

foo.call(obj2);

可以看到,第一次打印的是obj1,第二次打印的是obj2;也就是说第一次的this指针指向与obj这个对象,第二次this指针指向与obj2这个对象;

下面是代码:

function foo(){
alert(this.name);
}
var obj1 = {name:'obj1'};
var obj2 = new Object();
obj2.name = 'obj2';
foo.call(obj1); // obj1
foo.call(obj2); //obj2

我们在方法调用中能查询this引用以得到当前的实例,因此我们也能够使用的下面的代码来传送this的引用;

比如如下代码:

function foo(){
alert(this.name);
}
function MyObject(){
this.name = 'myObject';
}
MyObject.prototype.doAction = function(){
foo.call(this);
}
// 测试
var obj3 = new MyObject();
obj3.doAction();

如上代码先实例化MyObject这个对象,得到实例obj3, 然后调用实例的doAction这个方法,那么当前的this指针就指向了obj3这个实例,同时obj3.name = ‘MyObject’;  所以在调用foo.call(this)时,this指针指向与obj3这个实例,因此alert(this.name);就弹出myObject;

使用同样的方法,我们可以传递参数,代码如下:

function calc_area(w,h) {
alert(w*h);
}
function Area() {
this.name = 'MyObject';
}
Area.prototype.doCalc = function(v1,v2){
calc_area.call(this,v1,v2);
};
var area = new Area();
area.doCalc(10,20);

如上使用了call方法,并且给call方法传递了2个参数,但是上面的我们也可以使用apply()方法来调用,我们知道apply()方法和call()方法的不同点就是第二个参数,如上call方法的参数是一个一个的传递,但是apply的第二参数是一个数组或者是arguments,但是他们实现的功能是相同的;

function calc_area(w,h) {
alert(w*h);
}
function Area() {
this.name = 'MyObject';
}
Area.prototype.doCalc = function(v1,v2){
//calc_area.call(this,v1,v2);
calc_area.apply(this,[v1,v2])
};
var area = new Area();
area.doCalc(10,20);

理解javascript对象

Object.defineProperty方法, 该方法是ECMAScript5提供的方法,该方法接收3个参数,属性所在的对象,需要修改对象中属性名字,和一个描述符对象;描述符对象的属性必须是 configurable、enumerable、writable 和value。设置其中的一或多个值,可以修改对应的特性值。

ECMAScript中有2种属性,数据属性和访问器属性

  1.   数据属性;

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性;

configurable表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。这个特性值默认为true。

enumerable表示能否通过 for-in 循环返回属性。这个特性值默认为true。

writable表示能否修改属性的值。这个特性值默认为true。

value:  包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置上,这个特性值默认为undefined;

目前标准的浏览器支持这个方法,IE8-不支持此方法;

比如我们先定义一个对象person,如下:

var person = {
name: 'longen'
};

我们可以先alert(person.name); 打印弹出肯定是longen字符串;

理解writable属性;

现在我们使用Object.defineProperty()方法,对person这个对象的name属性值进行修改,代码如下:

alert(person.name); // longen
Object.defineProperty(person, "name", {
writable: false,
value: "tugenhua"
});
alert(person.name); // tugenhua
person.name = "Greg";
alert(person.name); // tugenhua

如上代码,我们writable设置为false的时候,当我们进行修改name属性的时候,是修改不了的,但是如果我把writable设置为true或者直接删掉这行代码的时候,是可以修改person中name的值的。如下代码:

Object.defineProperty(person, "name", {
writable: false,
value: "tugenhua"
});
alert(person.name); // tugenhua
person.name = "Greg";
alert(person.name); // Greg

理解configurable属性

继续如上JS代码如下:

var person = {
name: 'longen'
};
alert(person.name); // longen
Object.defineProperty(person, "name", {
configurable: false,
value: "tugenhua"
});
alert(person.name); // tugenhua
delete person.name;
alert(person.name); // tugenhua

当把configurable设置为false的时候,表示是不能通过delete删除name这个属性值的,所以上面的最后一个弹窗还会打印出tugenhua这个字符串的;

但是如果我把configurable设置为true或者直接不写这个属性的话,那么最后一个person.name弹窗会是undefined,如下代码:

var person = {
name: 'longen'
};
alert(person.name); // longen
Object.defineProperty(person, "name", {
value: "tugenhua"
});
alert(person.name); // tugenhua
delete person.name;
alert(person.name); // undefined

理解enumerable属性

Enumerable属性表示能否通过for-in循环中返回数据,默认为true是可以的,如下代码:

var person = {
name: 'longen'
};
Object.defineProperty(person, "name", {
enumerable: true,
value: "tugenhua"
});
alert(person.name); // tugenhua
for(var i in person) {
alert(person[i]); // 可以弹出框
}

如上是把enumerable属性设置为true,但是如果把它设置为fasle的时候,for-in循环内的数据就不会返回数据了,如下代码:

var person = {
name: 'longen'
};
Object.defineProperty(person, "name", {
enumerable: false,
value: "tugenhua"
});
alert(person.name); // tugenhua
for(var i in person) {
alert(person[i]); // 不会弹出
}

2.   访问器属性

访问器属性有getter和setter函数,在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值,在写入访问器属性时,会调用setter函数并传入新值,这个函数负责如何处理数据,访问器属性也有以下4个特性:

configurable表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。这个特性值默认为true。

enumerable表示能否通过 for-in 循环返回属性。这个特性值默认为true。

get在读取属性时调用的函数,默认值为undefined。

set在写入属性时调用的函数,默认值为undefined。

如下代码:

var book = {
_year: 2004,
edit: 1
};
Object.defineProperty(book,"year",{
get: function(){
return this._year;
},
set: function(newValue) {
if(newValue > 2004) {
this._year = newValue;
this.edit += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edit); //

首先我们先定义一个book对象,有2个属性_year和edit,并初始化值,给book对象再新增值year为2005,而访问器属性year则包含一个getter函数和一个setter函数,因此先会调用set函数,把2005传给newValue,之后this._year就等于2005,this.edit就等于2了;

目前支持Object.defineProperty方法的浏览器有IE9+,Firefox4+,safari5+,chrome和Opera12+;

理解定义多个属性

ECMAScript5定义了一个Object.defineProperties()方法,这个方法可以一次性定义多个属性,该方法接收2个参数,第一个参数是添加或者修改该属性的对象,第二个参数是一个对象,该对象的属性与第一个参数的对象需要添加或者删除的属性一一对应;

如下代码:

var book = {
_year: 2004,
edit: 1
};
Object.defineProperties(book,{
_year: {
value: 2015
},
edit: {
value: 2
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > this._year) {
this._year = newValue;
this.edit += newValue - this._year;
}
}
}
});

如上代码;给book对象设置了3个属性,其中前面两个会覆盖原有的book的对象的属性,三个属性是添加的;

上面确实是给对象设置了多个属性了,那么现在我们如何读取属性了?

ECMAScript5给我们提供了方法 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。该方法接收2个参数,属性所在的对象和需要读取描述符的属性名称。返回值也是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get 和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable 和value。

我们先来看下数据属性,如下代码获取:

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor);

打印出来如下:

Object {value: 2015, writable: true, enumerable: true, configurable: true}

是一个对象,现在的value变成2015了;

但是如果我们来看下访问器属性的话,如下代码:

var descriptor = Object.getOwnPropertyDescriptor(book, "year");

console.log(descriptor);

打印如下:

理解Javascript的动态语言特性

就有如上四个属性了;

Object.getOwnPropertyDescriptor方法,支持这个方法的浏览器有:

IE9+,firefox4+,safari5+,opera12+和chrome;

理解构造函数

function Dog(name,age) {
this.name = name;
this.age = age;
this.say = function(){
alert(this.name);
}
}

如上就是一个构造函数,它与普通的函数有如下区别:

  1. 函数名第一个首字母需要大写,为了区分是构造函数。
  2. 初始化函数的时候需要new下;任何函数,只要它是通过new初始化的,那么他们就可以把它当做构造函数;

比如如下初始化实例化2个对象:

var dog1 = new Dog("wangwang1",'10');
var dog2 = new Dog("wangwang2",'11');

那么我们现在可以打印console.log(dog1.say());打印出来肯定是wangwang1,console.log(dog2.say());打印出来是wangwang2;

dog1和dog2分别保存着Dog的一个不同的实例,且这两个对象都有一个constructor(构造函数)属性。该属性指向Dog,如下代码:

alert(dog1.constructor === Dog);  // true
alert(dog2.constructor === Dog); // true

同时实例化出来的对象都是Object的实例,可以通过instanceof 来检测如下代码:

alert(dog1 instanceof Object);    // true
alert(dog2 instanceof Object); // true
alert(dog1 instanceof Dog); // true
alert(dog2 instanceof Dog); // true

构造函数的缺点:就是每个方法都需要在每个实例上重新创建一遍,比如上面的实例化2个Dog对象,分别为dog1和dog2,dog1和dog2都有一个say方法,但是那个方法不是同一个Function的实例,如下代码:

alert(dog1.say === dog2.say);  // false

因此我们需要引入原型模式;原型模式就是要解决一个共享的问题,我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是让所有的实例共享属性和方法;比如还是上面的代码改造成如下:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
dog1.say();
var dog2 = new Dog();
dog2.say();
alert(dog1.say === dog2.say); // true

如上打印 dog1.say === dog2.say,他们共享同一个方法;为什么会是这样的?

我们可以先来理解下原型对象;

不管什么时候,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象;比如上面的函数Dog,那么就会为该函数创建一个Dog.prototype 这么一个对象,在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性.

构造函数的任何一个实例都会指向与该构造函数的原型;比如我们可以通过isPrototypeOf()方法来确定对象是否存在这种关系;如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(Dog.prototype.isPrototypeOf(dog1));//true
console.log(Dog.prototype.isPrototypeOf(dog1));//true

也就是说每个实例内部都有一个指针指向与Dog.prototype;ECMAScript5增加了一个新方法,Object.getPrototypeOf();这个方法可以返回属性值;还是如上面的代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(Object.getPrototypeOf(dog1) === Dog.prototype); //true
console.log(Object.getPrototypeOf(dog1).name);//wangwang

使用Object.getPrototypeOf()可以方便地取得一个对象的原型,而这在利用原型实现继承的情况下是非常重要的。

支持这个方法的浏览器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+和Chrome。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

比如如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var dog2 = new Dog();
dog1.name = "aa";
console.log(dog1.name);// aa
console.log(dog2.name);// wangwang

还是我们刚刚上面说的,对象查找的方式是查找2次,先查找实例中有没有这个属性,如果对象的实例有这个属性的话,直接返回实例中的属性值,否则的话继续查找原型中的属性值,如果有则返回相对应的值,否则的话返回undefined,如上我们先给dog1的实例一个name属性,那么再次查找的话,那么查找的是实例中的name属性,但是实例dog2查找的还是原型中的name属性;但是如果我们需要让其查找原型的name属性的话,我们可以使用delete删除这个实例中的name属性;如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var dog2 = new Dog();
dog1.name = "aa";
console.log(dog1.name);// aa
console.log(dog2.name);// wangwang
delete dog1.name;
console.log(dog1.name);// wangwang

但是我们可以使用hasOwnProperty()方法可以检测一个属性是存在实例中,还是存在原型中,如下测试代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var dog2 = new Dog();
console.log(dog1.hasOwnProperty("name")); // false
dog1.name = "aa";
console.log(dog1.name); // aa
console.log(dog1.hasOwnProperty("name")); // true
console.log(dog2.name); // wangwang
console.log(dog2.hasOwnProperty("name")); // false
delete dog1.name;
console.log(dog1.name); // wangwang
console.log(dog1.hasOwnProperty("name")); //false

理解原型与in操作符

有2种方式使用in操作符,单独使用和在for-in循环中使用,在单独使用中,in操作符会在通过对象访问给定属性时返回true,不管它是在实例中还是在原型中,比如如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
console.log("name" in dog1); // true
dog1.name = "aa";
console.log("name" in dog1); //true

上面代码中,name属性无论是在实例中还是在原型中,结果都返回true,我们可以通过in和hasOwnProperty()方法来确定属性是不是在原型当中,我们都知道in不管是在实例中还是在原型中都返回true,而hasOwnProperty()方法是判断是不是在实例中,如果在实例中返回true,那么我们取反就不在实例当中了;如下代码封装:

function hasPrototypeProperty(object,attr){
return !object.hasOwnProperty(attr) && (attr in object);
}

如下测试代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
function hasPrototypeProperty(object,attr){
return !object.hasOwnProperty(attr) && (attr in object);
}
console.log(hasPrototypeProperty(dog1,'name')); //true 在原型中
dog1.name = 'aa';
console.log(hasPrototypeProperty(dog1,'name')); //false 在实例中

for-in

在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包括在实例中的属性,也包括在原型中的属性;如果在IE8-下屏蔽了原型中已有的方法,那么在IE8-下不会有任何反应;如下代码:

var obj = {
toString: function(){
return "aa";
}
};
for(var i in obj){
if(i == "toString") {
alert(1);
}
}

如果我把上面的toString改成toString22的话,就可以在IE下打印出1,否则没有任何执行,那是因为他屏蔽了原型中不可枚举属性的实例属性不会在for-in循环中返回,因为原型中也有toString这个方法,在IE中,由于其实现认为原型的toString()方法被打上了值为false 的[[Enumerable]]标记,因此应该跳过该属性,结果我们就不会看到警告框。

要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5 的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

如下代码演示:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog()
var keys = Object.keys(Dog.prototype);
console.log(keys);//["name",'age','say']

如上代码;keys将保存为一个数组,这个顺序是在for-in出现的顺序,如果我们想要得到所有实例属性,无论它是否可枚举,我们可以使用 Object.getOwnPropertyNames()方法,如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
var keys = Object.getOwnPropertyNames(Dog.prototype);
console.log(keys);//["name",'age','say']

Object.keys()和Object.getOwnProperty-Names()方法都可以用来替代for-in 循环。支持这两个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera

12+和Chrome。

我们接下来再来理解下原型对象的概念;如下代码:

function Dog() {};
Dog.prototype = {
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof Dog); // true
console.log(dog1.constructor == Dog); // false
console.log(dog1.constructor == Object); // true

上面的第三行为什么会打印false呢?我们知道,每创建一个函数,就会同时创建它的prototype对象。这个对象会自动获得constructor属性,我们实例化一个对象的时候,那是因为我们没有给他指定constructor属性,默认情况下它会重写prototype对象,因此constructor属性也就变成了新对象的constructor属性了,不再指向Dog函数,如果我们需要让他还是指向与Dog函数的话,我们可以在Dog.property中添加constructor属性,如下代码:

function Dog() {};
Dog.prototype = {
constructor: Dog,
name: 'wangwang',
age:'11',
say: function(){
alert(this.name); //wangwang
}
}
var dog1 = new Dog();
console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof Dog); // true
console.log(dog1.constructor == Dog); // true

理解原型的动态性

比如如下代码:

function Dog() {};
var dog1 = new Dog();
Dog.prototype.say = function(){
alert(1);
}
dog1.say(); //

我们先实例化一个对象后,再给原型添加一个say方法,再我们使用实例调用该方法的时候也可以调用的到,这也就是说,实例会先搜索该say方法,如果没有搜索到,那么它会到原型里面去搜索该方法,如果能查找的到就执行,否则就会报错,没有这个方法;

虽然可以随时为原型添加方法和属性,且修改的方法和属性能从实例中表现出来,但是如果重写整个原型方法那就不行了;如下代码:

function Dog() {};
var dog1 = new Dog();
Dog.prototype = {
constructor: 'Dog',
name:'dog',
age:'1',
say: function(){
alert(this.age);
}
}
dog1.say(); // 报错
var dog1 = new Dog();

实例化一个对象时,会为该实例指向原型的指针,但是如果重写该原型的话,那么就会把该对象与原来的那个原型切断关系,那么继续调用该方法就会调用不到,如上面的代码,如果我再在重写该原型下面继续实例化该对象Dog,继续调用say方法就正常了;如下代码:

function Dog() {};
var dog1 = new Dog();
Dog.prototype = {
constructor: 'Dog',
name:'dog',
age:'1',
say: function(){
alert(this.age);
}
}
//dog1.say(); // 报错
var dog2 = new Dog();
dog2.say(); //

理解原型重写

我们从上面可知,原型是可以被重写的,那么原型重写后造成的问题就是会改变之前的实例指针指向原来的原型,那也就是说之前的原型假如有继承等操作的话,通过重写后的原型也会改变,所以在实际操作的时候要小心点,原型重写可以使同一个构造器实例出2个不同的实例出来;如下代码:

function MyObject(){};
var obj1 = new MyObject();
MyObject.prototype.type = 'myObject';
MyObject.prototype.value = "aa"; var obj2 = new MyObject();
MyObject.prototype = {
constructor: 'MyObject',
type: 'Brid',
value:'bb'
};
var obj3 = new MyObject();
// 显示对象的属性
alert(obj1.type); // myObject
alert(obj2.type); // myObject
alert(obj3.type); // Brid

如上代码:obj1与obj2两个实例是指向同一个原型的,obj3通过修改原型后,指向与新的构造函数的原型;如下测试代码:

// 显示实例的关系
alert(obj1 instanceof MyObject); // false
alert(obj2 instanceof MyObject); // false
alert(obj3 instanceof MyObject); // true

我们可能会有误解,为什么obj1 与 obj2不是MyObject的实例呢?我们从代码中确实可以看到,他们2个实例确实是MyObject的实例,那为什么现在不是呢?那我们现在再来看看对象实例constructor属性,如下测试代码:

console.log(obj1 instanceof obj1.constructor); // false
console.log(obj1.constructor === MyObject); // true

第一行打印false,可以看出 该对象obj1不是 obj1.constructor构造器,第二行打印true,obj1.constructor的构造器还是指向与MyObject对象;如下三行代码:

alert(obj1 instanceof MyObject); // false
console.log(obj1 instanceof obj1.constructor); // false
console.log(obj1.constructor === MyObject); // true

从上面的三行代码我们可以总结出,原型被重写后,obj1.constructor的构造器还是指向与MyObject,但是obj1不是obj1.constructor的构造器的实例,那就是说obj1不是MyObject的实例;

在javascript中,一个构造器的原型可以被重写,那就意味着之前的一个原型被废弃,在该构造器实例中:

  1. 旧的实例使用这个被废弃的原型,并受该原型的影响。
  2. 新创建的实例则使用重写后的原型,受新原型的影响。

理解构造器重写

上面我们了解了原型被重写,下面我们来讲解下构造器被重写,继承待会再来研究,我们先来看看构造器的重写demo,代码如下:

function MyObject(){};
var obj1 = new MyObject();
MyObject = function(){};
var obj2 = new MyObject();
console.log(obj1 instanceof MyObject); // false
console.log(obj2 instanceof MyObject); // true
console.log(obj1 instanceof obj1.constructor); // true

如上代码 obj1实例化出来对象被下面的MyObject构造器重写了,因此obj1不是MyObject的实例,obj2才是MyObject的实例,那obj1为什么是obj1.constructor的实例呢?说明构造器的重写不会影响实例的继承关系。

上面的构造器重写MyObject不是具名函数,下面我们再来看看具名函数的重写,代码如下:

function MyObject(){};
var obj1 = new MyObject();
function MyObject(){};
var obj2 = new MyObject();
console.log(obj1 instanceof MyObject); // true
console.log(obj2 instanceof MyObject); // true
console.log(obj1 instanceof obj1.constructor); // true

如上代码;从上面代码结构来看,obj1与obj2是2个不同的MyObject()构造器的实例,但是从逻辑上看,后面的MyObject()构造器其实是覆盖了前面的构造器,所以obj1与obj2都是第二个MyObject的构造器的实例;

因此上面打印的都是true;

原型对象的缺点:

  1. 省略了构造函数传递参数,所有实例在默认情况下都取得相同的属性值和方法,这并不好,比如我想A实例不需要自己的属性值,B实例需要有自己的属性值和自己的方法,那么原型对象就不能够满足需求;
  2. 原型最大的好处就是可以共享属性和方法,但是假如我给A实例化后添加一个方法后,我不想给B实例化添加对应的方法,但是由于原型都是共享的,所以在B实例后也有A中添加的方法;

对应第二点,我们可以看如下demo:

function Dog(){};
Dog.prototype = {
constructor: Dog,
name: 'aa',
values: ["aa",'bb'],
say: function(){
alert(this.name);
}
}
var dog1 = new Dog();
dog1.values.push("cc");
console.log(dog1.values); // ["aa","bb","cc"]
var dog2 = new Dog();
console.log(dog2.values); // ["aa","bb","cc"]

如上代码,我给实例化dog1的values再添加一个值为cc后,那么原型就变成

[“aa”,”bb”,”cc”]后,如果现在再实例化dog2后,那么继续打印dog2.values的值也一样为[“aa”,”bb”,”cc”];

理解组合使用构造函数模式和原型模式

构造函数模式用于定义实例私有属性,而原型模式可以定义共享的属性和方法,可以节省内存,同时可以有自己的私有属性和方法,这种方法模式使用的最广;比如如下代码:

function Dog(name,age){
this.name = name;
this.age = age;
this.values = ["aa",'bb'];
};
Dog.prototype = {
constructor: Dog,
say: function(){
alert(this.name);
}
}
var dog1 = new Dog("dog1",'12');
dog1.values.push("cc");
console.log(dog1.values); // ["aa","bb","cc"]
var dog2 = new Dog("dog2",'14');
console.log(dog2.values); // ["aa","bb"]
console.log(dog1.values === dog2.values);//false
console.log(dog1.say === dog2.say); //true

还有许多其他的模式,我这边不一一介绍,需要了解的话,可以看看Javascript设计模式那本书;

理解Javascript继承

一:原型链

ECMAScript中有原型链的概念,并将原型链作为继承的主要的方法,其思想是让一个引用类型去继承另一个引用类型的属性和方法,从上面我们了解到,原型和实例的关系,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,实例与构造函数本身没有什么关系,比如我们现在让一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个类型的指针,相应的,另一个原型中也包含着指向另一个构造函数的指针,那么层层递进,就成了原型链;

如下代码:

function Animal() {
this.name = "aa";
}
Animal.prototype.fly = function(){
return this.name;
};
function Dog() {
this.value = "bb";
}
Dog.prototype = new Animal();
Dog.prototype.fly = function(){
return this.name;
};
var dog1 = new Dog();
console.log(dog1.fly()); // aa

如上代码:我们先定义了一个Animal这个构造函数,它有一个属性name=”aa”; 且原型定义了一个方法fly; 接着我定义了Dog这么一个构造函数,且让其原型等于Animal的实例,也就是使用这种方式使Dog这个构造函数继承了Animal的属性和方法,因此Dog有Animal这个构造函数所有的属性和方法,接着再定义Dog的自己的fly方法,它会覆盖原型Animal的方法,且指针还是指向与Animal的,因此this.name =”aa”; 所以当我们实例化Dog的时候,访问dog1.fly()方法的时候,打印出aa;

如上代码我们知道如果想要A继承与B的话,那么继承可以这样写:

A.prototype = new B();

还有Dog的fly方法实际上是对原型Animal的fly方法进行重写;我们继续看看dog1实例与Dog与Animal的关系;如下代码:

console.log(dog1 instanceof Dog); // true
console.log(dog1 instanceof Animal); // true
console.log(dog1 instanceof dog1.constructor); // true
console.log(dog1.constructor === Dog); // false
console.log(dog1.constructor === Animal); // true

如上可以看到,dog1是Dog与Animal的实例,dog1还是指向与dog1.constructor,但是dog1的实例的constructor不再指向与Dog了,而是指向与Animal,这是因为dog1.constructor被重写了的缘故!

通过原型的继承,我们看到dog1.fly()方法,会经历如下几个搜索步骤,第一先搜索该实例有没有fly这个方法,接着搜索Dog的原型有没有这个方法,最后悔搜索Animal这个prototype这个;最后会继续看Object中有没有这个方法,我们都知道所有的对象都是Object的实例,我们可以看下:

console.log(dog1 instanceof Object); //true

所有函数默认的原型都继承与Object的实例,因此默认原型都有一个内部指针指向与Object.prototype; 那也就是说所有的自定义类型都会继承与toString()方法和valueOf()方法的根本原因,我们知道测试原型与实例的关系除了可以使用instanceof之外,我们还可以使用isPrototypeOf()方法, 如下代码:

console.log(Object.prototype.isPrototypeOf(dog1)); // true
console.log(Dog.prototype.isPrototypeOf(dog1)); // true
console.log(Animal.prototype.isPrototypeOf(dog1)); // true

注意:1. 子类型有时候需要重写超类型的某个方法,或者需要添加超类型中不存在的某个方法,给原型添加的方法一定要放在替换原型方法之后;如下代码:

function Animal() {
this.name = "aa";
}
Animal.prototype.fly = function(){
return this.name;
};
function Dog() {
this.value = "bb";
}
// 继承Animal
Dog.prototype = new Animal();
// 重写原型的方法
Dog.prototype.fly = function(){
return this.name;
};
// 给自身添加新方法
Dog.prototype.cry = function(){
return false;
};
var dog1 = new Dog();
console.log(dog1.fly()); // aa
console.log(dog1.cry()); // false

2 . 通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链;如下代码:

function Animal() {
this.name = "aa";
}
Animal.prototype.fly = function(){
return this.name;
};
function Dog() {
this.value = "bb";
}
// 继承Animal
Dog.prototype = new Animal();
// 重写原型的方法
Dog.prototype = {
fly: function(){
return this.name;
},
// 给自身添加新方法
cry: function(){
return false;
}
};
var dog1 = new Dog();
console.log(dog1.fly()); // undefined
console.log(dog1 instanceof Animal); // false

如上代码所示:打印dog1.fly()方法 打印出undefined, 打印 dog1 instanceof Animal 打印false,可知:不能使用对象字面量的方法来实现重写原型的方法,因为这样做会切断与原型Animal的关系,比如现在dog1 不是 Animal的实例,且dog1的实例没有fly这个方法,因为它现在不是继承了;

使用原型链的缺点如下:

1. 我们都知道原型链中所有的属性和方法都会被所有实例共享,虽然原型可以解决共享的问题,这是他的优点,但也是他的缺点,比如我给A实例添加一个属性,当我实例化的B的时候,B也有这个属性,如下代码:

function Animal() {
this.values = ["aa",'bb'];
}
function Dog(){};
Dog.prototype = new Animal();
var dog1 = new Dog();
dog1.values.push("cc"); // 添加cc值
console.log(dog1.values); // [“aa”,”bb”,”cc”];
var dog2 = new Dog();
console.log(dog2.values); // [“aa”,”bb”,”cc”];

2. 在创建子类型的实例中,不能向超类型中的构造函数传递参数。

理解借用构造函数

针对上面2点,因此我们需要借用于构造函数;其基本思想是:在子类型构造函数的内部调用超类型的构造函数,因此我们可以使用call或者apply的方法来调用,如下代码:

function Animal() {
this.values = ["aa",'bb'];
}
function Dog(){
// Dog继承于Animal
Animal.call(this);
};
var dog1 = new Dog();
dog1.values.push("cc"); // 添加cc值
console.log(dog1.values); // ['aa','bb','cc'] var dog2 = new Dog();
console.log(dog2.values); // ['aa','bb']

如上代码:使用call或者apply的方法实现继承,可以得到自己的副本values,因此第一次打印出[“aa”,’bb’,’cc’] 第二次打印出 [“aa”,’bb’];

我们也可以传递参数,代码如下:

function Animal(name) {
this.values = ["aa",'bb'];
this.name = name;
}
function Dog(){
// Dog继承于Animal
Animal.call(this,"dog22");
this.age = 22;
};
var dog1 = new Dog();
console.log(dog1.name); // dog22
console.log(dog1.age); //

但是呢,借用构造函数也有缺点;

借用构造函数的缺点:

  1. 构造函数不能复用;
  2. 在超类型中定义的属性或者方法,在子类型中是不可见的,结果所有类型都只能使用构造函数的模式;

理解组合继承

需要解决上面的2个问题,我们可以考虑使用组合继承的方式来实现,就是指构造函数模式与原型模式组合起来一起使用,其思想就是:使用原型链实现对原型的属性和方法的继承,而借用构造函数来实现对实例中的属性的继承;这样,既可以通过在原型上定义的方法实现函数的复用,又能保证每个实例都有自己的属性;如下代码:

function Animal(name) {
this.values = ["aa",'bb'];
this.name = name;
}
Animal.prototype.sayName = function(){
return this.name;
}
function Dog(name,age){
// Dog继承属性
Animal.call(this,name);
this.age = age;
};
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.sayAge = function(){
return this.age;
}
var dog1 = new Dog("dog111",'12');
dog1.values.push("cc");
console.log(dog1.values); // ['aa','bb','cc']
console.log(dog1.sayAge()); //
console.log(dog1.sayName()); // dog111 var dog2 = new Dog("dog222",'14');
console.log(dog2.values); // ['aa','bb']
console.log(dog2.sayAge()); //
console.log(dog2.sayName());// dog222

如上代码:Animal构造函数定义了2个属性,name和values,Animal原型中定义了一个方法sayName; Dog构造函数继承Animal是传递了参数name,然后又定义了自己的age参数,最后将Dog.prototpye = new Animal实例化Animal,让其Dog继承与Animal中的方法,这样的设计使Dog的不同的实例分别有自己的属性,同时又共有相同的方法,也节省了内存;

如上代码通过方法继承后,重写给Dog的constructor指向与Dog;如下代码:

Dog.prototype.constructor = Dog;

所以最后的Dog的实例对象的constructor都指向与Dog,我们可以打印如下:

console.log(dog1.constructor === Dog) // true

如果我们把上面的 Dog.prototype.constructor = Dog 注释掉的话,那么

console.log(dog1.constructor === Dog) // false

就返回false了;

理解原型式继承

其思想是:创建一个临时性的构造函数,然后将其传入的对象作为该构造函数的原型,最后返回这个临时构造函数的一个新实例,如下代码演示:

function object(obj) {
function F() {};
F.prototype = obj;
return new F();
}

我们现在可以做一个demo如下:

var person = {
name: 'aa',
firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = object(person);
anthorperson.name = "bb";
anthorperson.firends.push("zhaoliu");
var aperson2 = object(person);
aperson2.name = 'cc';
aperson2.firends.push("longen");
console.log(person.firends); // ["zhangsan", "lisi", "wangwu", "zhaoliu", "longen"];

这样的原型继承是必须有一个对象作为另一个对象的基础,如果有这么一个对象的话,可以把它传递object()函数;

ECMAScript5中新增Object.create()方法规范了原型式的继承,这个方法接收2个参数,第一个是用作新对象的原型的对象,第二个参数是可选的,含义是一个新对象定义额外属性的对象;比如如下代码:

var person = {
name: 'aa',
firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = Object.create(person);
anthorperson.name = "bb";
anthorperson.firends.push("zhaoliu"); var bperson = Object.create(person);
bperson.name = 'longen';
bperson.firends.push("longen");
console.log(person.firends); // ["zhangsan", "lisi", "wangwu", "zhaoliu", "longen"]

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
name: 'aa',
firends: ['zhangsan','lisi','wangwu']
};
var anthorperson = Object.create(person,{
name: {
value: 'bb'
}
});
console.log(anthorperson.name); //bb

目前支持Object.create()方法的浏览器有 IE9+,Firefox4+,Safari5+,Opera12+和chrome;

理解寄生组合式继承

前面我们理解过组合式继承,组合式继承是javascript最常用的继承模式,不过,它也有缺点,它会调用两次超类型的构造函数,第一次在继承属性的时候,调用,第二次在继承方法的时候调用,如下代码:

function Animal(name) {
this.values = ["aa",'bb'];
this.name = name;
}
Animal.prototype.sayName = function(){
return this.name;
}
function Dog(name,age){
// Dog继承属性
Animal.call(this,name);
this.age = age;
};
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
Dog.prototype.sayAge = function(){
return this.age;
}

如上面的继承属性;Animal.call(this,name);

和继承方法Dog.prototype = new Animal();

当第一次继承属性的时候,会继承Animal中的name和values,当第二次调用继承方法的时候,这次又在新对象中创建了实例属性name和values,这次创建的属性会覆盖之前继承的属性;因此我们可以使用寄生组合式继承;

寄生组合式继承的思想是:是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上是使用寄生式继承来继承超类型中的原型,然后再将结果指定给子类型的原型,寄生组合式的基本模式如下代码:

function inheritPrototype(Dog,Animal) {
var prototype = object(Animal.prototype);
prototype.constructor = Dog;
Dog.prototype = prototype;
}

inheritPrototype该方法接收2个参数,子类型构造函数和超类型构造函数,在函数内部,先创建一个超类型的一个副本,。第二步是为创建的副本添加constructor 属性,从而弥补因重写原型而失去的默认的constructor 属性。

最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用inherit-Prototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了,

如下代码演示:

function object(obj) {
function F() {};
F.prototype = obj;
return new F();
}
function inheritPrototype(Dog,Animal) {
var prototype = object(Animal.prototype);
prototype.constructor = Dog;
Dog.prototype = prototype;
}
function Animal(name) {
this.values = ["aa",'bb'];
this.name = name;
}
Animal.prototype.sayName = function(){
return this.name;
}
function Dog(name,age){
// Dog继承属性
Animal.call(this,name);
this.age = age;
};
inheritPrototype(Dog,Animal);
var dog1 = new Dog("wangwang",12);
dog1.values.push("cc");
console.log(dog1.sayName()); // wangwang
console.log(dog1.values); // ["aa", "bb", "cc"] var dog2 = new Dog("ww2",14);
console.log(dog2.sayName()); // ww2
console.log(dog2.values); // ["aa", "bb"]

如上使用寄生组合继承只调用了一次超类型;这就是他们的优点!