JavaScript的隐藏特性?

312

1
你是不是想说:“看到了那个问题吸引的声望和观点,我想问几乎完全相同的问题来提高自己的声望”?;-) - Bobby Jack
1
当然,悲观主义者。 :) 我考虑过将这个问题变成社区问题。此外,当你获得一定数量的积分后,所有的回报都会递减。 - Allain Lalonde
1
好的,看起来你并不“需要”声望!我猜我只是对C#这个问题有些困惑——它似乎并不是这个网站旨在解决的问题类型。 - Bobby Jack
3
是的,也许不是这样,但我发现答案中的知识非常棒。如果没有 Stack Overflow(SO),要让一般的 C# 程序员在一个地方接触到所有这些知识将会很难。玩耍多年才能获得同样的宝贵经验清单。 - Allain Lalonde
我喜欢这一系列的问题;我认为答案的“digg”式系统比论坛上的“+1”更好。更容易看出社区认为最重要的是什么。我相信这对于谷歌来说也是良好的链接诱饵! - Martin Clarke
7
我已经专业编写JavaScript 10年了,从这个帖子中我学到了一些东西。谢谢Alan! - Andrew Hedges
99个回答

43

原型继承(由道格拉斯·克罗克福德广为流传)完全革新了你在JavaScript中思考许多事情的方式。

Object.beget = (function(Function){
    return function(Object){
        Function.prototype = Object;
        return new Function;
    }
})(function(){});

这真是太棒了!可惜几乎没有人使用。

它可以让你"产生"任何对象的新实例并对其进行扩展,在保持与它们其他属性的(动态)原型继承链接的同时。例如:

var A = {
  foo : 'greetings'
};  
var B = Object.beget(A);

alert(B.foo);     // 'greetings'

// changes and additionns to A are reflected in B
A.foo = 'hello';
alert(B.foo);     // 'hello'

A.bar = 'world';
alert(B.bar);     // 'world'


// ...but not the other way around
B.foo = 'wazzap';
alert(A.foo);     // 'hello'

B.bar = 'universe';
alert(A.bar);     // 'world'

42

有些人可能会说这是品味问题,但:

aWizz = wizz || "default";
// same as: if (wizz) { aWizz = wizz; } else { aWizz = "default"; }

三元运算符可以链接起来,就像Scheme的(cond ...)一样:
(cond (predicate  (action  ...))
      (predicate2 (action2 ...))
      (#t         default ))

可以写成...

predicate  ? action( ... ) :
predicate2 ? action2( ... ) :
             default;

这非常“实用”,它可以在不产生副作用的情况下分支您的代码。因此,不要写成:

if (predicate) {
  foo = "one";
} else if (predicate2) {
  foo = "two";
} else {
  foo = "default";
}

您可以编写:

foo = predicate  ? "one" :
      predicate2 ? "two" :
                   "default";

递归使用也很好 :)


2
呃...JavaScript确实有switch()语句。 :-) - staticsan
14
我很欢迎三元运算符。 - thomasrutter
8
重新阅读后,我想指出这并不是“让代码看起来像另一种语言”,而是简化代码的语义含义:当你想说“将foo设置为三个中的一个”时,应该使用“foo = ...”这样的语句,而不是“if”。 - Andrey Fedorov
这很棒,但这是如何隐藏的?C语言已经存在很长时间了,我们一直在这样做。 - Hogan
三元链接在许多基于C的编程语言中都是可行的,这并不特定于JavaScript。 - Justin Johnson
显示剩余5条评论

41

数字也是对象。因此,您可以执行一些很酷的操作,例如:

// convert to base 2
(5).toString(2) // returns "101"

// provide built in iteration
Number.prototype.times = function(funct){
  if(typeof funct === 'function') {
    for(var i = 0;i < Math.floor(this);i++) {
      funct(i);
    }
  }
  return this;
}


(5).times(function(i){
  string += i+" ";
});
// string now equals "0 1 2 3 4 "

var x = 1000;

x.times(function(i){
  document.body.innerHTML += '<p>paragraph #'+i+'</p>';
});
// adds 1000 parapraphs to the document

哇!我不知道有toString(radix)这个方法... - Ates Goral
1
times 的实现不够高效:每次都会调用 Math.floor 而不是只调用一次。 - dolmen

33

JavaScript中的闭包(类似于C# v2.0+中的匿名方法)。您可以创建一个创建函数或“表达式”的函数。

闭包的示例:

//Takes a function that filters numbers and calls the function on 
//it to build up a list of numbers that satisfy the function.
function filter(filterFunction, numbers)
{
  var filteredNumbers = [];

  for (var index = 0; index < numbers.length; index++)
  {
    if (filterFunction(numbers[index]) == true)
    {
      filteredNumbers.push(numbers[index]);
    }
  }
  return filteredNumbers;
}

//Creates a function (closure) that will remember the value "lowerBound" 
//that gets passed in and keep a copy of it.
function buildGreaterThanFunction(lowerBound)
{
  return function (numberToCheck) {
    return (numberToCheck > lowerBound) ? true : false;
  };
}

var numbers = [1, 15, 20, 4, 11, 9, 77, 102, 6];

var greaterThan7 = buildGreaterThanFunction(7);
var greaterThan15 = buildGreaterThanFunction(15);

numbers = filter(greaterThan7, numbers);
alert('Greater Than 7: ' + numbers);

numbers = filter(greaterThan15, numbers);
alert('Greater Than 15: ' + numbers);

1
我不确定,但是可以把 (numberToCheck > lowerBound) ? true : false; 简化为 return (numberToCheck > lowerBound); 只是想增加我的理解... - davidsleeps
4
我认为C#中的匿名函数相当于闭包,而不是闭包相当于匿名函数。 :) - vava
11
闭包和匿名函数是不同的概念。可以创建没有名称的函数,这就是匿名函数。在“创建”作用域中的变量与创建的函数相关联,这就是闭包。简而言之,闭包更像是一个隐藏的全局变量。 - slebetman
1
那是对的。只有在匿名方法使用创建范围中的变量时,它才类似于闭包。我已经更新了答案的英文。它仍然有所欠缺,但我无法确定正确的英语表达方式。 - Tyler
2
我认为这不是最好或最易理解的闭包示例。只是说一下。闭包的重点在于,即使一堆变量似乎“超出范围”,它们仍然可以对最初在该范围内定义的函数保持可用性。在上面的示例中,这意味着lowerBound变量仍然可以被该内部匿名函数访问,即使外部函数buildGreaterThanFunction终止。 - thomasrutter
@thomasrutter +1 - 我使用闭包来“捕获”稍后使用的范围(例如在setTimeout或事件处理程序中),例如: function testFunc(){ var str='当超时事件发生时,这将超出范围'; setTimeout(function(){ return function(input){ alert(input); }(str); }, 5000); return; } - acatalept

32

您还可以使用原型链 spoon16 提到的方式来扩展(继承)类并重写属性/方法。

在下面的示例中,我们创建了一个 Pet 类并定义了一些属性。 我们还覆盖了从 Object 继承的 .toString() 方法。

接下来,我们创建了一个 Dog 类,它扩展自 Pet 并再次重写了 .toString() 方法,改变了它的行为(多态性)。此外,我们还向子类添加了一些其他属性。

之后我们检查继承链以展示 Dog 仍然是 Dog 类型、Pet 类型和 Object 类型。

// Defines a Pet class constructor 
function Pet(name) 
{
    this.getName = function() { return name; };
    this.setName = function(newName) { name = newName; };
}

// Adds the Pet.toString() function for all Pet objects
Pet.prototype.toString = function() 
{
    return 'This pets name is: ' + this.getName();
};
// end of class Pet

// Define Dog class constructor (Dog : Pet) 
function Dog(name, breed) 
{
    // think Dog : base(name) 
    Pet.call(this, name);
    this.getBreed = function() { return breed; };
}

// this makes Dog.prototype inherit from Pet.prototype
Dog.prototype = new Pet();

// Currently Pet.prototype.constructor
// points to Pet. We want our Dog instances'
// constructor to point to Dog.
Dog.prototype.constructor = Dog;

// Now we override Pet.prototype.toString
Dog.prototype.toString = function() 
{
    return 'This dogs name is: ' + this.getName() + 
        ', and its breed is: ' + this.getBreed();
};
// end of class Dog

var parrotty = new Pet('Parrotty the Parrot');
var dog = new Dog('Buddy', 'Great Dane');
// test the new toString()
alert(parrotty);
alert(dog);

// Testing instanceof (similar to the `is` operator)
alert('Is dog instance of Dog? ' + (dog instanceof Dog)); //true
alert('Is dog instance of Pet? ' + (dog instanceof Pet)); //true
alert('Is dog instance of Object? ' + (dog instanceof Object)); //true

这个问题的两个答案都是从Ray Djajadinata的一篇优秀的MSDN文章修改而来的代码。


31

你可以根据异常的类型捕获它们。引自Mozilla 开发者网络(MDN)

try {
   myroutine(); // may throw three exceptions
} catch (e if e instanceof TypeError) {
   // statements to handle TypeError exceptions
} catch (e if e instanceof RangeError) {
   // statements to handle RangeError exceptions
} catch (e if e instanceof EvalError) {
   // statements to handle EvalError exceptions
} catch (e) {
   // statements to handle any unspecified exceptions
   logMyErrors(e); // pass exception object to error handler
}

注意:条件捕获子句是Netscape(因此也是Mozilla/Firefox)的扩展,不是ECMAScript规范的一部分,因此除了特定的浏览器之外,不能依赖它。


29
我会尽力而为:抓住我如果你能。 - Ates Goral
6
请阅读您引用的 MDC 页面上的注释:条件捕获语句是 Netscape(因此也是 Mozilla/Firefox)的扩展,不是 ECMAScript 规范的一部分,因此除非在特定浏览器上,否则不能依赖它。 - Jason S

31

光是凭记忆而言...

函数

arguments.callee 指向包含 "arguments" 变量的函数,因此它可以用于递归匿名函数:

var recurse = function() {
  if (condition) arguments.callee(); //calls recurse() again
}

如果你想做类似于这样的事情,那么这就非常有用:

//do something to all array items within an array recursively
myArray.forEach(function(item) {
  if (item instanceof Array) item.forEach(arguments.callee)
  else {/*...*/}
})

对象

关于对象成员的一个有趣的事情是:它们可以使用任何字符串作为名称:

//these are normal object members
var obj = {
  a : function() {},
  b : function() {}
}
//but we can do this too
var rules = {
  ".layout .widget" : function(element) {},
  "a[href]" : function(element) {}
}
/* 
this snippet searches the page for elements that
match the CSS selectors and applies the respective function to them:
*/
for (var item in rules) {
  var elements = document.querySelectorAll(rules[item]);
  for (var e, i = 0; e = elements[i++];) rules[item](e);
}

字符串

String.split可以接受正则表达式作为参数:

"hello world   with  spaces".split(/\s+/g);
//returns an array: ["hello", "world", "with", "spaces"]

String.replace可以将正则表达式作为搜索参数并将函数作为替换参数:

var i = 1;
"foo bar baz ".replace(/\s+/g, function() {return i++});
//returns "foo1bar2baz3"

你提到的这些东西... 它们在所有浏览器中都实现了吗? - cllpse
4
不。我相当确定Mosaic缺少它们中的大部分。 - jsight
2
JavaScript的功能在所有主要浏览器中都得到了实现(IE6/7、FF2/3、Opera 9+、Safari2/3和Chrome)。 document.querySelectorAll在所有浏览器中尚未得到支持(它是W3C版本的JQuery的$()和Prototype的$$())。 - Leo
6
arguments.callee 在 ECMAScript 5 中已被弃用,将抛出异常。 - Hello71
并不完全正确。一个对象键不能(或者更确切地说,不应该)使用字符串“hasOwnProperty”作为名称,因为那会覆盖内置的对象方法。 - Breton

29

大多数情况下,你可以使用对象代替 switch 语句。

function getInnerText(o){
    return o === null? null : {
        string: o,
        array: o.map(getInnerText).join(""),
        object:getInnerText(o["childNodes"])
    }[typeis(o)];
}

更新:如果您担心提前评估的情况效率低下(为什么在程序设计早期就担心效率?),那么可以这样做:

function getInnerText(o){
    return o === null? null : {
        string: function() { return o;},
        array: function() { return o.map(getInnerText).join(""); },
        object: function () { return getInnerText(o["childNodes"]; ) }
    }[typeis(o)]();
}

这种写法比使用switch或对象更费力(或阅读),但它保留了使用对象而不是switch的好处,这些好处在下面的注释部分有详细解释。这种写法还使将其发展成一个合适的“类”更加直接。

更新2:随着ES.next提出的语法扩展,这将变得更加简单:

let getInnerText = o -> ({
    string: o -> o,
    array: o -> o.map(getInnerText).join(""),
    object: o -> getInnerText(o["childNodes"])
}[ typeis o ] || (->null) )(o);

3
这就是Python没有开关语句的方式。 - outis
2
问题在于它总是评估所有情况。 - Kornel
@porneL 这是真的,但它带来了一些好处:逻辑更清晰:case 是字符串,在哈希表中查找,而不是每个表达式都必须被评估以确定是否相等。因此,虽然会评估更多的“值”,但会评估较少的“键”。对象可以动态生成,并进行修改以实现后期可扩展性,反射以打印 UI 或生成文档,甚至可以用动态的“查找”函数替换,这比复制/粘贴 case 更好。没有关于 break、fall-through 或默认值的混淆。可以进行 JSON 序列化... - Breton
@porneL 噢,对了,关于可扩展性,一个对象甚至可以轻松地转换为外部配置或数据文件,这比使用 switch 语句更加简单明了——但是,如果一开始就设计了对象,那么这个过程也不会太麻烦。 - Breton
我知道这是一个晚期的条目,但除非您有一些自定义类型检查逻辑,否则数组何时能够与您的示例一起工作呢? var arr = []; typeof arr; // object - keeganwatkins
太对了,不过请注意在最后一个例子中我使用了"typeis(o)",但实际上typeof只是我用作临时示例的东西,与主要观点相比较边缘化。我将编辑其他示例以匹配。 - Breton

25

在迭代一个对象的属性时,请务必使用 hasOwnProperty 方法:

for (p in anObject) {
    if (anObject.hasOwnProperty(p)) {
        //Do stuff with p here
    }
}

这样做是为了让你仅访问anObject直接属性,而不使用原型链下的属性。


23

使用自调函数定义的巧妙技巧实现了具有公共接口的私有变量。 返回的对象中的所有内容均可在公共接口中使用,而其他内容则为私有。

var test = function () {
    //private members
    var x = 1;
    var y = function () {
        return x * 2;
    };
    //public interface
    return {
        setx : function (newx) {
            x = newx;
        },
        gety : function () {
            return y();
        }
    }
}();

assert(undefined == test.x);
assert(undefined == test.y);
assert(2 == test.gety());
test.setx(5);
assert(10 == test.gety());

1
这被称为模块模式,由Eric Miraglia在http://yuiblog.com/blog/2007/06/12/module-pattern/上命名。我认为这个名称有误导性,应该被称为单例模式或类似的东西。我还可以补充说,公共方法也可以通过使用“this”对象调用其他公共方法。我经常在我的代码中使用这种模式来保持组织和清洁。 - mikeycgto

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接