深入解析JavaScript函数、this指向与原型链机制
本文深入解析JavaScript中函数作为对象的特性、this关键字在不同调用上下文中的指向规则,以及原型链的工作原理。通过对比构造函数直接赋值与原型共享方法的内存差异,探讨如何利用prototype优化对象实例,并指出原型中引用类型共享可能引发的数据污染问题及解决方案。
JavaScript函数作为对象的特性
关于函数
在JavaScript中,函数本质上是一个对象,具备引用类型的特征。因此,你可以将函数直接赋值给变量,该变量将持有指向函数“对象”的指针。例如:
function test(message){
alert(message);
}
var f = test;
f('hello world');
你也可以直接将函数表达式赋值给变量:
var f = function(message){
alert(message);
};
f('hello world');
在此种情况下,函数声明中可以省略函数名称,因为此时名称已无实际意义。我们可以直接通过变量f来调用该函数。
借助Function构造函数,我们可以更深入地理解“函数即对象”这一概念:
var f = new Function("message","alert(message);");
f('hello world');
深入理解this关键字的作用域
关于this
this关键字可以视为调用函数时的实际作用域上下文。通过对比以下函数的执行结果,可以更清晰地理解这一点:
function test(){
this.property = 'hello world';
}
test();
alert(window.property); //由于在全局范围内调用,test函数中的this实际指向全局对象(window)
var obj = {};
test.call(obj); //通过call第一个参数指定执行上下文范围,所以test函数中this指向obj实例。
alert(obj.property);
var obj2 = {};
obj2.test2 = test; //将obj2实例方法test指向 全局test方法
obj2.test2(); //由于是在obj2上调用test方法,所以test函数中的this也指向了obj2实例
alert(obj2.property);
构造函数与普通函数的区别
定义类型
在JavaScript中可以定义构造函数。构造函数与普通函数在语法上并无区别,但在创建实例时,若使用了new关键字,则该函数具备构造函数的特性;否则,它仅作为普通函数执行。如下所示,我们定义了一个Person类型:
function Person(){
this.name = 'xfrog';
this.Say = function(){
alert(this.name);
};
}
当使用new关键字时,可以创建一个新的Person对象实例:
var p1 = new Person();
p1.Say();
如果不使用new关键字,将直接执行Person函数。由于此时执行上下文为全局范围,故name属性和Say方法将被添加到window对象上:
Person();
Say();
window.Say();
原型链机制与内存优化
原型
注意上述Person的定义方式。当使用new来创建Person实例时,会执行Person构造函数,即每次都会声明name属性和Say方法。这可能导致效率问题,请留意以下代码:
var p1 = new Person();
var p2 = new Person();
var test = p1.Say == p2.Say;
比较p1和p2两个Say函数指针,返回false,表示每个Person实例中的Say方法都是独立的。事实上,Say函数的功能完全相同,我们完全没有必要为每个对象重新分配Say函数“对象”。如果Person实例数量庞大,将会造成大量的内存消耗。
如果将Say函数提取出来放入全局执行范围,似乎可解决此问题:
function Person(){
this.name = 'xfrog';
this.Say = say;
}
function say(){
alert(this.name);
}
var p1 = new Person();
var p2 = new Person();
alert(p1.Say == p2.Say);
p1.name = 'wang';
p1.Say();
由于this始终和执行上下文相关,p1和p2实例中的Say方法中会正确地返回对应实例的name属性。但是,使用此方式有违面向对象的思想,也失去了类型封装的原则。还会造成大量的全局函数污染。
为了解决这些缺点,JavaScript引入了原型的概念。简单理解,原型可以看作是类型的共享区。原型本身是一个对象,而对象中的属性对于类型来说是共享的。JavaScript中每个类型通过prototype属性来表示原型,通过这个属性可指定共享方法:
function Person(){
}
Person.prototype.name = 'xfrog';
Person.prototype.Say = function(){
alert(this.name);
};
var p1 = new Person();
var p2 = new Person();
alert(p1.Say == p2.Say); //返回true
为什么这里可以通过p1.Say来访问Say方法呢?这是因为ECMAScript标准规定了类型属性的查找顺序:先在类型的实例上查找,如果没有则继续在类型原型上查找。这一查找路径采用短路算法,即找到首个后即返回。考虑如下代码:
function Person(){
this.name = 'wang';
}
Person.prototype.name = 'xfrog';
Person.prototype.Say = function(){
alert(this.name);
}
var p1 = new Person();
p1.Say(); //将返回wang
访问内部原型对象
上面提到prototype实际上是一个对象,那么我们是否可以直接访问呢?在一些浏览器实现(如Chrome、Firefox等)中,确实可通过实例的__proto__属性来访问内部的prototype对象。这种特征表明JavaScript引擎在每个对象的内部都是通过一个变量来保存对prototype的引用,这保证了prototype对应整个类型的实例来说是共享的。例如,你可在Chrome浏览器内使用如下方式来访问Say方法:
p1.__proto__["Say"]();
替换原型对象的注意事项
由于原型是一个对象,我们可以直接将一个对象赋值给prototype:
function Person(){
}
Person.prototype = {name:'xfrog', Say:function(){
alert(this.name);
}};
注意这个方式下,实际上是完全替换了Person的prototype,这与上面Person.prototype.name方式还是有细微差异的。这是因为任何类型,JavaScript引擎都会添加默认的prototype,在这个prototype中包含一个对构造函数的引用,即原型对象属性constructor。所以通常使用替代prototype方式时,我们需要手动加上constructor属性:
Person.prototype = {
constructor: Person,
name :'xfrog',
Say:function(){
alert(this.name);
}
}
原型中引用类型的共享风险
注意,由于prototype对于整个类型是共享的,那么在prototype中的引用类型可能会存在问题。前面的Say函数作为一个对象,也是引用类型,所以每个实例中的Say都指向原型对象中的同一个函数,这本身没有问题,也是我们使用原型的初衷。但对于其他引用对象,可能结果并不是我们想要的:
function Person(){
}
Person.prototype = {
name: 'xfrog',
obj : { age: 18 },
Say : function(){
alert(this.obj.age);
}
};
var p1 = new Person();
var p2 = new Person();
p1.obj.age = 20;
p1.Say();
p2.Say();
p2.Say返回的是20,这是因为obj属性作为原型属性是共享的,在内存中只存在一个实例。所以通过p1修改后,p2只能得到修改后的状态。如果要避免此情况,可将obj属性放到实例中:
function Person(){
this.obj = { age: 18 };
}
以上就是小编为大家带来的浅谈Javascript中的函数、this以及原型全部内容了,希望大家多多支持本站~
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
