JavaScript 中的疑难杂点

写了一两年的JavaScript ,但是一直有几个模棱两可的点困扰着我。于是利用最近的闲余时间消化一下这几点。

1》JavaScript 的作用域

JavaScript 中的作用域只存在一种--公用作用域。JavaScript 中的所有对象的所有属性和方法都是公用的。

JavaScript 也没有静态作用域。不过,它可以给构造函数提供属性和方法。构造函数只是函数。函数是对象,对象可以有属性和方法。

JavaScript 标准规定了类型属性的查找顺序:先在类型的实例上查找,如果没有则继续在类型原型上查找,这一查找路径采用短路算法,即找到首个后即返回。

JavaScript 语言特有“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。父对象的所有变量,对子对象都是可见的,反之则不成立。

if (true) {
	var val = 'hello world!';
}
console.log(val); // 输出 hello world!
查看以上代码在java或者c/c++等语言里面,这是不合法的,但在javascript是完全支持这么做的。JavaScript 的作用域完全是由函数来决定,而不是块来决定。例如

var val = 'hello world!';
function scope(){
	console.log(val); // 输出 undefinded
	var val = 'hello JavaScript!';
}
scope();

对于其他语言来说,应该是输出hello world!,而结果却匪夷所思。这就是JavaScript的类型属性查找顺序的问题。首先会在scope()函数内部查询val是否被定义,如果有,上层作用域内的val就会被屏蔽。所以在执行console.log的时候,val其实是未被定义的。下面看一个更复杂的例子

var closure = function() {
	var scope = 'scope1';
	(function() {
		var scope = 'scope1';
		(function() {
			console.log(scope); // 输出 undefined
			var scope = 'scope2';
			(function() {
				console.log(scope); // 输出 scope2
			})();
		})();
	})();
};
closure();

2》闭包

按照链式作用域的概念,如果我要在函数外部引用函数内部的变量,这是没法做到的。但是我们可以改进一些函数的实现方式,例如

function outer(){
	var i=0;
	function inner(){
		console.log("第" + ++i +"次执行!");
	}
	return inner;
}
var result = outer();
result();//第1次执行!
result();//第2次执行!
result();//第3次执行!

或者

var outer = null;
(function(){
    var i = 0;
    function inner (){
        console.log(++i);
    }
    outer = inner;
})();
outer();    //1
outer();    //2
outer();    //3
在outer外部的变量result可以引用outer内部的inner函数,而inner就是闭包,他是一个函数内部的函数,是将函数内部和函数外部连接起来的一座桥梁。

闭包有三个重要的作用,在上面的例子当中,result能够读取outer内部的变量i,这是他第一个作用;第二个作用就是储存变量,让变量值始终能停留在内存中。比如i的值在执行第一次result后,没有被释放掉,所以每执行一次后,i都会加1。之所以出现这个原因是,在outer执行完成后,垃圾回收机制GC不会回收outer我占用的内存,因为inner的执行还依赖于outer的变量i;第三个作用就是制造类成员的私有特性和封装性,例如jquery,prototype等js框架都是使用闭包的。这样外界就不会破坏框架内部的架构。例如

function Closure() {  
    var id;  
    this.getId = function() {  
        return id;  
    }  
    this.setId = function(nid) {  
        id = nid;  
    }  
}  
var c = new Closure();  
c.setId(1);  
console.log(c.getId()); // 1
console.log(c.id); // undefined 
在外部id是无法访问的,而可以使用setter方式设置值,并用getter方式获取,这有点java,c/c++等高级语言封装性的意思。

闭包有一个严重的问题就是容易造成内存泄露。一般情况下,当对象无用的时候就会自动GC,但当javascript对象出现循环引用的时候,就不会被释放。比如三个对象ABC,他们的引用关系如下为A->B->C->B :这里增加了C的某一属性引用B对象,如果这是清除A,那么B、C不会被释放,因为B和C之间产生了循环引用。而闭包就是一种隐蔽的循环引用。

3》call和apply

call 和 apply 的功能是以不同的对象作为上下文来调用某个函数。简而言之,就是允许一个对象去调用另一个对象的成员函数。

1、每个函数都包含两个非继承而来的方法:apply()和call()。 
2、他们的用途相同,都是在特定的作用域中调用函数。 
3、接收参数方面不同,apply()接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
call()方法第一个参数与apply()方法相同,但传递给函数的参数必须列举出来。 apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始),如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj。。如 func.call(func1,var1,var2,var3)对应的apply写法为:func.apply(func1,[var1,var2,var3])。下面给出一个例子,让不具备display方法的类student具有了user类的display方法。

function user(name,age)
{
	this.name = name;
	this.age = age;
	this.display = function(){
		console.log("I'm " + name + ",I'm " + age);
	}
}
function student(name,age){
	// user.apply(this,arguments);
	user.call(this,name,age);
}
var s = new student("Fly",26);
s.display();//I'm Fly,I'm 26

通过上面的例子,我们发现apply和call可以实现类继承。而这种方式在extjs里面的运用特别多。在extjs中,还有一个applyif,这个跟apply有那么一点区别,比如说在student中已经包含了name属性的话,user中的name属性是不会被复制过来的。

apply最神奇的运用在数组方面。看下面的例子

var arr = [5,6,1,3,7];
console.log(Math.max(5,6,1,3,7)); //7
console.log(Math.max(arr)); //NaN
console.log(Math.max.apply(0,arr)); //7
在js内置的Math对象里面已经实现了数组的对比,但是是以参数的方式传值的,直接把数组传递进去会出现错误。如果一定要这么传值,那必须得重写max方法,但是我们有apply这个神器,简简单单解决这个问题。由于第一个参数一定得存在,我们可以传递任意参数代替作用域。包括global,数字或者null。
4》bind

如何改变被调用函数的上下文呢?前面说过,可以用 call 或 apply 方法,但如果重复使用会不方便,因为每次都要把上下文对象作为参数传递,而且还会使代码变得不直观。针对这种情况,我们可以使用 bind 方法来永久地绑定函数的上下文,使其无论被谁调用,上下文都是固定的。下面看msdn上的例子程序:

var originalObject = {
    minimum: 50,
    maximum: 100,
    checkNumericRange: function (value) {
        if (typeof value !== 'number')
            return false;
        else
            return value >= this.minimum && value <= this.maximum;
    }
}
console.log(originalObject.checkNumericRange(10));//false
var range = { minimum: 10, maximum: 20 };
var boundObjectWithRange = originalObject.checkNumericRange.bind(range);
console.log(boundObjectWithRange (15));//true
这边是bind的第一个作用,修改函数调用时的this指针。当boundObjectWithRange调用时,通过bind方法,this指针指向的是range对象。下面再看下我们传递参数的例子:

var displayArgs = function (val1, val2, val3, val4) {
    console.log(val1 + " " + val2 + " " + val3 + " " + val4);
}
var emptyObject = {};
var displayArgs2 = displayArgs.bind(emptyObject, 12, "a");
displayArgs2("b", "c"); //12 a b c

对于bind应该注意两点:在使用 new 操作符调用 bind 创建的新函数时,this 不会被修改,但是参数还是会修改;bind 并不被所有浏览器支持,IE 目前不支持。

5》原型prototype

先看一个例子程序,这是我在项目中用到的。在数组中获取某元素的索引值,而在默认的Array定义中是没有这个方法的。

Array.prototype.getIndexByValue = function(value)
{
	var index = -1;
	for (var i = 0; i < this.length; i++)
	{
		if (this[i] == value)
		{
			index = i;
			break;
		}
	}
	return index;
};
console.log([2,3,4,6,8].getIndexByValue(6)); //3
prototype意为对象的类返回原型的引用。每一个类都有一个prototype实例。我们可以在类型上使用proptotype来为类型添加行为。这些行为只能在类型的实例上体现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值