题主所说的代码形式就是为了在语言层面不支持默认参数(default parameter)功能时的变通做法。正因为它的意图是“在参数没有得到赋值时给它一个默认值”,所以常常会赋值回到原本名字的参数上(而不是新开一个别的名字的局部变量)。
在ES2015(ES6)语言层面添加了对默认参数的支持后,这种做法的必要性就大幅降低了。可参考MDN文档的介绍:
Default parameters注意上面MDN文档中给出的使用ES6语言层面默认参数之前的解决办法:
function multiply(a, b) { var b = typeof b !== 'undefined' ? b : 1; return a*b; } multiply(5); // 5
这个参数检查显然比题主给的例子的“param = param || {}”更复杂。
MDN的范例版本反映了ES6语言层面的默认参数的语义——只有当传入的参数为语言内建的undefined值时才使用默认参数值,否则使用传入的参数;
而题主给的例子则是当传入参数为任意假值(undefined、null、0等等)时都采用默认参数值。这比MDN的范例的限制更宽松,因而也更有可能出现意想不到的情况。谨慎使用。
如果一个实际场景就是期待传入的参数是个对象,如题主的例子那样,那么直接用 || {}其实也够用——假值都过滤掉就正好符合需求。
=====================================
题主所说的第1和第2种情况其实没有任何区别。要干净一点的话第1种更干净。
请参考
Annotated ES5 - 10.5 Declaration Binding Instantiation中的第4.d点和第8点。第4.d点讲解了函数的参数被放进环境的过程,而第8点则讲解了局部变量被创建和放进环境的过程——注意它强调只有当环境中一个名字还没有被声明的时候,才会创建该名字的局部变量。
(注意关于varAlredyDeclared的部分。这个规定保证了同一作用域内var变量不会被重复声明——重复的声明会等价于在开头只声明了一次。)
换句话说,这个规定也就指定了JavaScript的变量声明提升(variable declaration hoisting)的语义。在一个函数里,一个名字的变量无论被用var声明多少次,它们实际语义都等价于被合并在一起提升到函数开头的地方声明。
在声明的意义上,函数的参数就跟局部变量一样,只不过函数的参数早于所有局部变量的声明而被记录进环境里。这就是为什么题主给的第2种代码实际上跟第一种代码的意思是完全一样的——后面的var param并不会重复声明变量,也不会声明一个新的同名局部变量去遮蔽之前参数所声明的那个。
特别注意“声明”(declaration)和“定义”(definition)是不同的,前者只负责变量名在环境中的存在与否,而后者才是真正的赋值动作。举例说:
function foo() { var a = 1; // declaration + definition var a; // declaration only var a = 3; // declaration + definition a = 4; // definition only }
根据JavaScript的规定,带有初始化表达式(initialiser)的变量声明既是声明也是定义。在变量声明提升后,上述代码的实际效果跟下面的代码是一样的:
function foo() { var a; // declaration (hoisted) a = 1; // definition // var a; // (hoisted) a = 3; // definition a = 4; // definition }
留意这里变量声明经过提升就只有一份有效果了,但变量定义(赋值)的每一份都还在其原本的位置上起作用。可以参考
Annotated ES5 - 12.2 Variable Statement的规定:
A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.
=====================================
另外要注意在ES2015(ES6)里的let/const声明与之前的var声明的规定不一样了,var声明的局部变量还是函数作用域的,而let/const声明的binding则是词法块作用域的。但在合并声明的意义上let/const与var有相似之处——都会检测作用域里是否已经有该名字的变量声明。
var在同一作用域里的多个同名声明会被合并,而不会创建新变量或遮蔽之前的同作用域同名变量;
而let/const则会检测当前作用域里是否已经有该名字的声明,并且拒绝重复的声明。
在JavaScript这种闭包实现了引用捕获的语言里,要验证多个声明是否创建了多个变量很简单——让多个闭包捕获变量,最后看它们捕获的值是否相同:
function foo() { var a = 1 var f1 = function () { return a } var a = 2 var f2 = function () { return a } return { f1: f1, f2: f2 } } var o = foo() console.log(o.f1()) // 2 console.log(o.f2()) // 2
可以看到foo()里的f1与f2嵌套函数捕获到的a是同一个变量a,所以它们后面被调用时返回的a值一样。
如果把这个例子的变量a声明改为用let声明,
function foo() { let a = 1 var f1 = function () { return a } let a = 2 let f2 = function () { return a } // Uncaught SyntaxError: Identifier 'a' has already been declared return { f1: f1, f2: f2 } }
则该函数被加载时就会报错,拒绝同一作用域里同名变量的重复声明。
参数的处理同理:
function bar(x) { let x; // Uncaught SyntaxError: Identifier 'x' has already been declared }