javascript是目前web领域中实用最为广泛的语言,不管是在前端还是在后端都能看到它的影子,可以说web从业者不论怎样都绕不开它。在前端领域,各种框架层出不穷,最火的时候几乎每个月都有新的框架诞生,如angularjs,vuejs等。在后端领域,nodejs可谓如火如荼,打破了人们对javascript只能作为前端语言的认知。按照此势头下去,javascript会越来越流行,会随着web的发展越来越重要。现在基本没有第二种语言可以挑战js在web前端中的地位,至少10年以内不可能。
所以不论你是想学各种前端框架还是nodejs,都需要深入理解javascript的工作原理以及特性,只有这样才能以不变应万变。最近看了一些js的教学视频与NC的《JS的高级程序设计》这本书,在这里总结一下js的一些特性,以防自己忘记,也可以方便各位朋友学习与交流。
javascript是Netscape(网景)公司推出的浏览器端语言,与java没有半点关系,可能想接着java炒火吧。然后js越来越火,网景公司想将其标准化,这样更加利于网络的发展,遂将其提交给了ECMA(欧洲计算机制造商协会)管理,负责对其的标准化。ECMA机构以JS为原型,推出了一个ECMAScript的脚步语言,规定各大浏览器厂商都必须依照ECMAScript标准实现各种的JS,保证JS具有良好的跨平台性。所以可以将ECMAScript看成是标准化的JS,一个意思。
ECMAScript本质上是一种语言规范,其与平台没有关系,比如浏览器等。web浏览器只是ES的宿主环境之一,负责实现ES以及提供ES与环境交互的手段。宿主环境还有Node以及Flash等。
JS的实现要比ES规定的要复杂的多,ES只规定了基本语言特性。浏览器下的JS实现可以由下面三部分组成:
- 语言部分(ES)
- 文档对象模型(DOM)
- 浏览器对象模型(BOM)
DOM是负责操作由XML编写的应用程序的API,负责将整个页面映射成多层节点结构。如下所示:
DOM可以提供JS对节点结构的任何操作,增删改查等。根据DOM提供的功能多样性,将DOM分为DOM1,DOM2,DOM3这几个级别。DOM1由DOM Core与DOM HTML组成。DOM2在DOM1的基础上提供了更多的操作与功能。DOM3则更进一步的扩展了DOM。
BOM是浏览器对象模型,负责提供浏览器与JS的交互接口,提供JS操作浏览器的窗口与框架等。这个没啥好说的!
总之要实现一个完整的浏览器端JS,这个三个部分缺一不可。
javascript是一门动态语言,即在编写好代码后不用编译,由js解释器解释执行,同时变量不用显式的写出类型,统一用var类型表示,具体的变量类型由JS解释器推测,与python和ruby一样。说到js,大家经常听到面向函数式编程,这是js的一大设计特性。强大的function。其实在js中,函数本质上也是对象,也继承自Object类,也有属性等。js中也很多地方需要我们注意,它与java和C++很不一样。
1. js中没有类继承关键字,和java与C++不一样。js的类继承需要自己动手实现,这也衍生出了多种类继承的编写范式。
2. 同时js中没有函数重载特性,这个需要特别注意。因为在js中函数只是普通对象,没有函数签名(函数名+参数)。而在java和C++中,用函数签名唯一标示一个函数。不过在js中我们也可以有多种方式模拟出函数重载的效果。
3. js中的作用域与java也不一样,js中有作用域链,在函数执行中,解释器会根据执行函数的作用域链一层层的往上寻找变量,一直找到位于末端的window作用域中。
4. js中没有块级作用域。
for (var i = 0; i < 10; i++) {
}
alert(i);
上述代码是一个简单的for循环,在java等语言中,因为有块级作用域,所以i变量会在for循环执行完后消失。但是在js中,i变量会保持在执行环境中,因为没有块级作用域。所以alert出的结果是10。
5. js中有原型的概念,每个类都有对于的原型,包括函数等。类对象中有引用指向原型对象,所以同一类的原型对象被所有类对象共享。由此衍生出很多有意思的特性。
6. js中有闭包,这个闭包特性是由作用域链的设计衍生出来的,特别值得注意。根据闭包特性,结合匿名函数,我们可以模拟块级作用域效果,甚至可以模拟出单例模式以及私有变量等。
7. js中的继承与多态,需要程序员自己实现,与java和C++不一样。利用js的原型链,可以写出很多不同的继承效果,各有特点。写js中的继承远比java中有技术含量,哈哈!
8. js有垃圾回收机制,但是比较简单,没有jvm中的有意思。
执行环境是js中一个重要的概念。执行环境定义了对象或函数可以访问到的数据。每一个执行环境都有一个与之关联的变量对象,环境定义的所以变量和函数都保存在这个对象中。程序编写者无法正常访问该对象,但是后台的解析器会访问到它。
全局执行环境是最外围的一个执行环境,在web浏览器中是window对象。当某个执行环境执行完后,环境会被销毁,与之相关联的变量对象中的所有变量与函数也可能会被销毁。为什么用可能呢?有一个值得注意的地方,该变量对象销不销毁最本质的是看有没有其他引用指向它,如果有别的引用指向该变量对象,那么该变量对象不会被销毁,比如在闭包中(函数中的函数情况)。
当代码在一个环境中执行时,解析器会创建变量对象的一个作用域链。作用域链的最顶端始终是该执行环境的变量对象。对于函数而言,变量对象是其活动对象!当在执行环境中遇到一个变量时,解析器会从作用域链的最顶端变量对象中找相应的变量,没有找到,则会顺着作用域链一直找下去,最后找到全局执行环境的变量对象!最后还是没有,则会报错!
看下来一个函数:
function compare(value1, value2) {
return value2 - value1;
}
上面的函数执行在全局环境中,调用compare()时,会创建一个包含arguments、value1以及value2的对象,this特殊的变量不能在变量对象中找到。所以,全局执行环境中的变量对象则处在compare函数的作用域链中的第二位。如下图所示:
全局变量对象始终存在,像compare函数这样的局部环境的变量对象,只有在函数执行时才存在。首先创建compare函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在Scope属性中,Scope属性保存在compare函数对象中。当调用compare函数时,会创建一个执行对象,然后复制scope中的作用域链,将本地函数的活动对象放入作用域链的顶部。注意作用域链是多个引用的数组,本身不保持对象,所以复制作用域链不在内存且非常快速。
原型链是JS中实现继承的主要方式。每一个JS类中都有一个指向该类原型对象的引用。该原型对象有一个constructor属性,指向构造函数,如图所示:
由该类生成的对象中,也有个隐含的prototype属性,指向该类的原型对象!设想一下,我们如何将另一个类的对象实例作为某个类的原型对象会怎么样呢?如下所示:
可以看到对于instance对象而言,prototype指向的是SubType类的原型,而SubType类的原型是SuperType的类实例,其中的prototype指向SuperType类的原型对象,这样就形成了一个3层的原型链(包含Object原型)。当解析器在instance中寻找变量时,它会先在实例对象中寻找,然后沿着原型链一直向上寻找,直到找到为止!最终如果在Object类的原型中都没有找到,那么会产生错误!
如图所示:
根据原型链,我们可以写出JS中的继承代码,一般推荐用混合方式实现,即构造函数与原型链混合方式:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
这样就完成了继承的编写,混合方式的好处,可以避免只用原型链方式的一些缺点,比如不能向构造函数中传递参数,或者对于引用类型的值,不能做到对象独有一份!但是上式方式依然有缺点,可以发现,每次创建SubType类时,会调用SuperType的构造函数,创建两个变量,name与colors。但是我们会发现这两个变量其实在SubType的原型中已经存在了,只是SubType的对象实例中的变量屏蔽了其原型对象中的两个变量!这样一来,造成空间浪费,同时也耗费了在形成SubType原型对象中调用SuperType的构造函数的时间。
那么如何解决上述问题呢,我们可以用寄生混合式继承方法。代码如下:
function inheritPrototype(subType, superType) {
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
SubType.prototype = inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
采用这种寄生混合式的继承方法,使用寄生式继承父类的prototype对象,将结果做子类的prototype,这样就可以避免调用父类的构造函数,同时需要将prototype对象的constructor属性指向子类构造函数即可。
这种方法是目前公认的最理想的继承范式,能正常使用instanceof和isPrototypeOf()。
思考:这种方式就没有缺点吗?如果后续我们在SuperType的原型对象中增加一个方法,但是SubType的原型是复制品,所以后续的SubType对象实例中不能得到该方法。但是如果采用原型式继承+混合式继承呢?能不能得到更好的效果呢?思考下面这段代码:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name, age) {
this.age = age;
SuperType.call(this, name);
}
//原型继承SuperType的原型对象
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.sayAge = function() {
alert(this.Age);
}
SuperType A = new SuperType("hh");
SubType B = new SubType("MM", 26);
//增加这段代码
SuperType.prototype.sayColors = function() {
alert(this.colors);
}
B.sayColors();
闭包是JS中的一个非常重要的概念。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包常见的方式是在一个函数内部建立另一个函数,例如:
function createCompare(propertyName) {
return function(object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
return value2 - value1;
}
}
var compare = createCompare("name");
var result = compare({name: "haha"}, {name: "hehe"});
该函数返回一个匿名函数,在该匿名函数中可以访问外面函数的活动变量propertyName。该原理是:在匿名函数返回以后,匿名函数的作用域链被初始化为包含createCompare()函数的活动对象以及全局变量对象。这样,在匿名函数执行时就可以访问createCompare函数的活动变量了。值得注意的是,createCompare函数在执行完毕后,它的变量对象并没有被销毁,因为有匿名函数的作用域链依然在引用这个活动对象。换句话说,createCompare函数执行完后,它的作用域链会被销毁,但是它的活动变量却保留在了内存中,直到匿名函数被销毁后,它的活动对象才会被销毁。用下图表示:
java中有匿名对象,js中有匿名函数,其实本质都差不多。js中函数对象都有一个name属性。对于name属性,其实是指向函数声明时跟在function后面的名字。但是在匿名函数中name为空字符串。这里需要正确理解函数声明与函数表达式。
函数声明:
functionName(a, b); //由于函数声明提升,所以可以执行
function functionName(arg0, arg1) {
}
函数表达式:
a(); //函数表示式,,没有函数声明提升,所以不能执行,报错!
var a = function(arg0, arg1) {
};
在递归情况下,可以用匿名函数很好的书写,即使在严格模式下,依然可以使用:
var factorial = (function f(num) {
if (num <= 1)
return 1;
else
return num * f(num-1);
});
用命名函数表达式,可以将f()函数赋值给factorial变量,但是函数的名字依然是f,可以测试factorial.name依然是f。
有了匿名函数,可以模拟块级作用域:
(function(){
//模拟的块级作用域
})();
在括号内用函数声明,表示这个是函数表达式,后面紧接括号,表示立刻调用这个匿名函数。
匿名函数配合闭包特性,可以实现单例模式:
var singleton = (function() {
//设置私有变量与私有函数
var privateVariable = 10;
function privateFunction(){
alert("hello world");
}
//创建对象,可以是任意类型的对象
var object = new Object();
//添加特权/公有属性与方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
return object;
})();