JavaScript数组indexOf与Object.defineProperty机制解析

作者:青柠时间:2026-10-30 06:20:11浏览:396

本文深入探讨JavaScript中数组与对象的关系,从模拟IE8以下浏览器的indexOf方法入手,解析call和apply在模拟继承中的应用。重点讲解Object.defineProperty如何控制属性读写、只读及配置权限,并演示如何利用该特性使DOMTokenList支持数组方法。

JavaScript数组与对象的基础关系

在JavaScript中,数组可以通过Array构造函数创建,但更推荐使用[]字面量方式,这也是首选做法。数组本质上是继承自Object原型的对象,因此typeof运算符对数组的返回值并非特殊类型,而是统一返回'object'。

在JS语境下,万物皆对象,数组本身也是一种特殊的对象类型。

许多内置对象提供了便捷的方法,如数组的push、concat和slice等。然而,若某些对象未实现这些方法,我们仍希望复用这些功能,该如何处理?

模拟数组indexOf方法

第一种思路是仿照现有高效方法的实现逻辑,自行编写类似功能。

例如,在IE8及更早版本中,浏览器不支持Array的indexOf方法。为了使旧版数组支持该功能,我们需要手动实现一个indexOf方法。

可通过IE浏览器调试工具(按F12)选择兼容模式至IE5进行验证。


var arr = [, , , ];
if (Array.prototype.indexOf) {
alert("你的浏览器支持indexOf方法。");
} else {
alert("你的浏览器不支持indexOf方法。");
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(item) {
for (var i = ; i < this.length; i++) {
if(this[i]==item){
return i;
}
}
return -;
}
}
alert(arr.indexOf());
alert(arr.indexOf()); 

当然,这种手动实现的方式较为粗糙。此处不详细展开具体实现细节,提供一个从网络获取的参考版本。

若感兴趣,可查阅V8引擎中array.js的实现源码:https://github.com/v8/v8/blob/master/src/js/array.js


if (!Array.prototype.indexOf)
{
Array.prototype.indexOf = function(elt /*, from*/)
{
var len = this.length >>> ;
var from = Number(arguments[]) || ;
from = (from < )
? Math.ceil(from)
: Math.floor(from);
if (from < )
from += len;
for (; from < len; from++)
{
if (from in this &&
this[from] === elt)
return from;
}
return -;
};
} 

利用call和apply模拟继承

第二种思路是利用call和apply方法实现类似继承的效果。

如果每个对象都需要单独编写实现,显然效率低下且繁琐。

在Java等高级语言中,可通过继承机制解决此类问题,如下面的Java代码所示。


public class MyList<E> extends ArrayList<E>
{
public void myAdd(E e){
super.add(e);
System.out.println("Add:"+e);
}
} 

虽然JavaScript没有传统的继承概念,但我们可以借助call和apply方法达到类似目的。

上述Java代码可改写为JS实现如下。


var myObject = function(){
}
myObject.prototype.add = function(){
Array.prototype.push.call(this,arguments);
//输出arguments
for(var i=;i<arguments.length;i++){
console.log("Add:"+arguments[i]);
}
}
var obj = new myObject();
obj.add(,,); 

可见,虽然用高级语言的继承方式实现了myAdd方法,但现在myAdd方法只能传一个参数,如果要传多个参数,则需要再写一个public void myAdd(E[] e)方法,甚至是public void myAdd(List<E> e)方法。而JS用一个方法就可以搞定,用arguments对象表示输入的所有参数,这是高级语言难以做到的。

(ps,其实在java中可以写public void myAdd(E... e),这个是不定参数,用法上public void myAdd(E[] e)是一样的)

call和apply方法用于改变函数内this指针的指向,call只有两个参数,而apply通常是知道参数个数之后才使用的,下面以例子说明:


var Obj = function(name){
this.name = name;
}
Obj.prototype.getName = function(){
return this.name;
}
var obj1 =new Obj("zou");
var obj2 = {name:'andy'};
var name = obj1.getName.call(obj2);
alert(name); 

参考是:

apply(object,arg1,arg2,....) call(object,[arg1,arg2,....])

call后面只能跟一个“数组”,包括了所有的参数。而apply则是一颗语法糖,如果知道参数的个数,用apply将很方便。

上面的object也可以是null或者undefined,这样,这个object就是global object(window),例如,还是接着上例:


var name = 'goo';
alert(obj1.getName.call(null)); 
(在严格模式下,由于全局对象是null,故会抛出异常:Uncaught TypeError: Cannot read property 'name' of null)

深入Object.defineProperty属性控制

3、Object.defineProperty

(注意:不要在IE8以下使用该类特性)

微软:将属性添加到对象,或修改现有属性的特性。

getter、setter,

其实js中对于对象的属性也有getter和setter函数,不过个人觉得js中的getter和setter更像C#一些。

例如下面的代码就定义了一个getter/setter:


function myobj(){
}
Object.defineProperty(myobj.prototype,'length',{
get:function(){
return this.length_; //这里不能是length。
},
set:function(value){
return this.length_=value;
}
}); 

注释的地方不能是length,否则会无限递归。

也可以去掉set,让length变量只读。


Object.defineProperty(myobj.prototype,'length',{
get:function(){
return this.length_; //这里不能是length。
}, 
/*set:function(value){
return this.length_=value;
}*/
});
myobj.length = 3; 

这个代码会抛出异常:Uncaught TypeError: Cannot set property length of #<myobj> which has only a getter。

要让对象的属性只读,还可以用writable:false,


Object.defineProperty(myobj.prototype,'length',{
writable:false
});

writable:false不能与get set共存,否则会抛出Type Error。

configurable:是否能用delete语句删除,但是configurable属性好像在严格模式下才有效,这样的代码在非严格模式下仍然能执行:(严格模式报错)


Object.defineProperty(myobj.prototype,'length',{
configurable:false
});
var obj = new myobj();
delete obj.length; 

value:指定该对象的固定值。value:10,表示这个对象初始值为10.

在非严格模式下,这样的代码不会报错,严格模式下会报错:


Object.defineProperty(myobj.prototype,'length',{
writable:false,
value:'10'
});
var obj = new myobj();
obj.length = 100; 

可以用getOwnPropertyDescriptor来获取并修改这些值,比如说,现在我的length属性是只读的。

运行这样的代码,结果却报错了:


Object.defineProperty(myobj.prototype,'length',{
value:,
writable:false,
});
var descriptor = Object.getOwnPropertyDescriptor(myobj.prototype, 
"length");
descriptor.writable = true;
Object.defineProperty(myobj.prototype,'length',descriptor); 
Uncaught TypeError: Cannot redefine property: length

这是因为configurable的默认值是false,在调用了defineProperty之后,configurable就具有false属性,这样就不能逆转了。以后就不能改了。

所以必须使用 configurable:true,这个对象属性才是可以修改的,完整的代码如下:


Object.defineProperty(myobj.prototype,'length',{
value:,
writable:false,
configurable:true
});
var descriptor = Object.getOwnPropertyDescriptor(myobj.prototype, 
"length");
descriptor.writable = true;
Object.defineProperty(myobj.prototype,'length',descriptor);
myobj.prototype.length = ;
var obj = new myobj();
alert(obj.length); 

可以加上一句descriptor.configurable = false;

表示这个属性我修改了,以后你们都不能再修改了

修改DOMTokenList以支持数组方法

这个特性在很多时候也有用,数组Array的push pop等方法,如果使用call、apply,要求对象的length可变。如果对象的length属性只读,那么调用call、apply时,会抛出异常。

就比如DOMTokenList对象,它的length就是不可以变的。我拿到了一个DOM对象DOMTokenList,

但是它的configurable是true,我们可以修改让它的length属性可以变啊:

JavaScript数组与对象的基础关系

看见没,这个configurable是true,而setter是undefined,我们给它写一个set方法,不就可以了吗?


var descriptor = Object.getOwnPropertyDescriptor(DOMTokenList.prototype,'length');
descriptor.set = function(value){
this.length = value;
}
Object.defineProperty(DOMTokenList.prototype,'length',descriptor); 

然后运行,

又抛出了一个异常,Uncaught RangeError: Maximum call stack size exceeded(…)

这是因为,我们在set this.length时,它会在我们写的那个set方法中无限递归。

因此,我们需要使用delete消除length属性的影响,也就是:


var descriptor = Object.getOwnPropertyDescriptor(DOMTokenList.prototype,'length');
descriptor.set = function(value){
delete DOMTokenList.prototype.length;
this.length = value;
}
Object.defineProperty(DOMTokenList.prototype,'length',descriptor); 

这样,DOMTokenList也就支持了push,pop等等操作了。


Array.prototype.push.call(document.body.classList,'abc')

然后再行封装


DOMTokenList.prototype.push = function(){
Array.prototype.push.call(document.body.classList,Array.prototype.slice.call(arguments));
}

Array.prototype.slice.call(arguments)方法用于把arguments对象转换为数组。

作者最新文章