一文探索 JavaScript 最强大特性—函数表达式

news2023/11/30 8:14:10

函数是 ECMAScript 中最有意思的部分之一,这主要是因为函数实际上是对象。每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义,比如:

function sum (num1, num2) {
  return num1 + num2;
}

注意函数定义最后没有加分号。

另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的:

let sum = function(num1, num2) {
  return num1 + num2;
};

这里,代码定义了一个变量 sum 并将其初始化为一个函数。注意 function 关键字后面没有名称,因为不需要。这个函数可以通过变量 sum 来引用。注意函数定义最后没有加分号。

注意这里的函数末尾是有分号的,与任何变量初始化语句一样。

还有一种定义函数的方式与函数表达式很像,叫作“箭头函数”(arrow function),如下所示:

let sum = (num1, num2) => {
  return num1 + num2;
};

最后一种定义函数的方式是使用 Function 构造函数。这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。来看下面的例子:

let sum = new Function("num1", "num2", "return num1 + num2");  // 不推荐

我们不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规 ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。不过,把函数想象为对象,把函数名想象为指针是很重要的。而上面这种语法很好地诠释了这些概念。

注意 这几种实例化函数对象的方式之间存在微妙但重要的差别,本章后面会讨论。无论如何,通过其中任何一种方式都可以创建函数。

箭头函数

ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:

let arrowSum = (a, b) => {
  return a + b;
};

let functionExpressionSum = function(a, b) {
  return a + b;
};

console.log(arrowSum(5, 8)); // 13
console.log(functionExpressionSum(5, 8)); // 13

箭头函数简洁的语法非常适合嵌入函数的场景:

let ints = [1, 2, 3];

console.log(ints.map(function(i) { return i + 1; }));  // [2, 3, 4]
console.log(ints.map((i) => { return i + 1 }));        // [2, 3, 4]

如果只有一个参数,那也可以不用括号。只有没有参数,或者多个参数的情况下,才需要使用括号:

// 以下两种写法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };

// 没有参数需要括号
let getRandom = () => { return Math.random(); };

// 多个参数需要括号
let sum = (a, b) => { return a + b; };

// 无效的写法:
let multiply = a, b => { return a * b; };

箭头函数也可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那么箭头后面就只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值:

// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; };
let triple = (x) => 3 * x;

// 可以赋值
let value = {};
let setName = (x) => x.name = "Matt";
setName(value);
console.log(value.name); // "Matt"

// 无效的写法:
let multiply = (a, b) => return a * b;

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

函数名

因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称,如下所示:

function sum(num1, num2) {
  return num1 + num2;
}

console.log(sum(10, 10));         // 20

let anotherSum = sum;
console.log(anotherSum(10, 10));  // 20

sum = null;
console.log(anotherSum(10, 10));  // 20

以上代码定义了一个名为 sum() 的函数,用于求两个数之和。然后又声明了一个变量 anotherSum,并将它的值设置为等于 sum。注意,使用不带括号的函数名会访问函数指针,而不会执行函数。此时,anotherSum 和 sum 都指向同一个函数。调用 anotherSum() 也可以返回结果。把 sum 设置为 null 之后,就切断了它与函数之间的关联。而 anotherSum() 还是可以照常调用,没有问题。

ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":

function foo() {}
let bar = function() {};
let baz = () => {};

console.log(foo.name);               // foo
console.log(bar.name);               // bar
console.log(baz.name);               // baz
console.log((() => {}).name);        //(空字符串)
console.log((new Function()).name);  // anonymous

如果函数是一个获取函数、设置函数,或者使用 bind() 实例化,那么标识符前面会加上一个前缀:

function foo() {}

console.log(foo.bind(null).name);    // bound foo

let dog = {
  years: 1,
  get age() {
    return this.years;
},
set age(newAge) {
  this.years = newAge;
 }
}

let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name);  // get age
console.log(propertyDescriptor.set.name);  // set age

理解参数

ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。

之所以会这样,主要是因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。如果数组中什么也没有,那没问题;如果数组的元素超出了要求,那也没问题。事实上,在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。

arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数,可以访问arguments.length属性。

在下面的例子中,sayHi() 函数的第一个参数叫 name:

function sayHi(name, message) {
  console.log("Hello " + name + ", " + message);
}

可以通过 arguments[0] 取得相同的参数值。因此,把函数重写成不声明参数也可以:

function sayHi() {
  console.log("Hello " + arguments[0] + ", " + arguments[1]);
}

在重写后的代码中,没有命名参数。name 和 message 参数都不见了,但函数照样可以调用。这就表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。与其他语言不同,在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。

也可以通过 arguments 对象的 length 属性检查传入的参数个数。下面的例子展示了在每调用一个函数时,都会打印出传入的参数个数:

function howManyArgs() {
  console.log(arguments.length);
}

howManyArgs("string", 45);  // 2
howManyArgs();              // 0
howManyArgs(12);            // 1

这个例子分别打印出 2、0 和 1(按顺序)。既然如此,那么开发者可以想传多少参数就传多少参数。比如:

function doAdd() {
  if (arguments.length === 1) {
    console.log(arguments[0] + 10);
  } else if (arguments.length === 2) {
    console.log(arguments[0] + arguments[1]);
  }
}

doAdd(10);      // 20
doAdd(30, 20);  // 50

这个函数 doAdd() 在只传一个参数时会加 10,在传两个参数时会将它们相加,然后返回。因此 doAdd(10) 返回 20,而 doAdd(30,20) 返回 50。虽然不像真正的函数重载那么明确,但这已经足以弥补 ECMAScript 在这方面的缺失了。

还有一个必须理解的重要方面,那就是arguments对象可以跟命名参数一起使用,比如:

function doAdd(num1, num2) {
  if (arguments.length === 1) {
    console.log(num1 + 10);
  } else if (arguments.length === 2) {
    console.log(arguments[0] + num2);
  }
}

在这个 doAdd() 函数中,同时使用了两个命名参数和 arguments 对象。命名参数 num1 保存着与 arugments[0] 一样的值,因此使用谁都无所谓。(同样,num2 也保存着跟 arguments[1] 一样的值。)

arguments 对象的另一个有意思的地方就是,它的值始终会与对应的命名参数同步。来看下面的例子:

function doAdd(num1, num2) {
  arguments[1] = 10;
  console.log(arguments[0] + num2);
}

这个 doAdd() 函数把第二个参数的值重写为 10。因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1] 也会修改 num2 的值,因此两者的值都是 10。但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。另外还要记住一点:如果只传了一个参数,然后把 arguments[1] 设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。

对于命名参数而言,如果调用函数时没有传这个参数,那么它的值就是 undefined。这就类似于定义了变量而没有初始化。比如,如果只给 doAdd() 传了一个参数,那么 num2 的值就是 undefined。

严格模式下,arguments 会有一些变化。首先,像前面那样给 arguments[1] 赋值不会再影响 num2 的值。就算把 arguments[1] 设置为 10,num2 的值仍然还是传入的值。其次,在函数中尝试重写 arguments 对象会导致语法错误。(代码也不会执行。)

箭头函数中的参数

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问。

function foo() {
  console.log(arguments[0]);
}
foo(5); // 5

let bar = () => {
  console.log(arguments[0]);
};
bar(5);  // ReferenceError: arguments is not defined

虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:

function foo() {
  let bar = () => {
    console.log(arguments[0]); // 5
  };
  bar();
 }

 foo(5);

注意 ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用。

没有重载

ECMAScript 函数不能像传统编程那样重载。在其他语言比如 Java 中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。

如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。来看下面的例子:

function addSomeNumber(num) {
  return num + 100;
}

function addSomeNumber(num) {
  return num + 200;
}

let result = addSomeNumber(100); // 300

这里,函数 addSomeNumber() 被定义了两次。第一个版本给参数加 100,第二个版本加 200。最后一行调用这个函数时,返回了 300,因为第二个定义覆盖了第一个定义。

前面也提到过,可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。

把函数名当成指针也有助于理解为什么 ECMAScript 没有函数重载。在前面的例子中,定义两个同名的函数显然会导致后定义的重写先定义的。而那个例子几乎跟下面这个是一样的:

let addSomeNumber = function(num) {
return num + 100;
};

addSomeNumber = function(num) {
return num + 200;
};

let result = addSomeNumber(100); // 300

看这段代码应该更容易理解发生了什么。在创建第二个函数时,变量 addSomeNumber 被重写成保存第二个函数对象了。

默认参数值

在 ECMAScript5.1 及以前,实现默认参数的一种常用方式就是检测某个参数是否等于 undefined,如果是则意味着没有传这个参数,那就给它赋一个值:

function makeKing(name) {
  name = (typeof name !== 'undefined') ? name : 'Henry';
  return `King ${name} VIII`;
}

console.log(makeKing());         // 'King Henry VIII'
console.log(makeKing('Louis'));  // 'King Louis VIII'

ECMAScript 6 之后就不用这么麻烦了,因为它支持显式定义默认参数了。下面就是与前面代码等价的ES6写法,只要在函数定义中的参数后面用=就可以为参数赋一个默认值:

function makeKing(name = 'Henry') {
  return `King ${name} VIII`;
}

console.log(makeKing('Louis'));  // 'King Louis VIII'
console.log(makeKing());         // 'King Henry VIII'

给参数传 undefined 相当于没有传值,不过这样可以利用多个独立的默认值:

function makeKing(name = 'Henry', numerals = 'VIII') {
  return `King ${name} ${numerals}`;
}

console.log(makeKing());                 // 'King Henry VIII'
console.log(makeKing('Louis'));          // 'King Louis VIII'
console.log(makeKing(undefined, 'VI'));  // 'King Henry VI'

在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。当然,跟 ES5 严格模式一样,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准:

function makeKing(name = 'Henry') {
  name = 'Louis';
  return `King ${arguments[0]}`;
}

console.log(makeKing());         // 'King undefined'
console.log(makeKing('Louis'));  // 'King Louis'

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:

let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
let ordinality = 0;

function getNumerals() {
  // 每次调用后递增
  return romanNumerals[ordinality++];
}

function makeKing(name = 'Henry', numerals = getNumerals()) {
  return `King ${name} ${numerals}`;
}

console.log(makeKing());                // 'King Henry I'
console.log(makeKing('Louis', 'XVI'));  // 'King Louis XVI'
console.log(makeKing());                // 'King Henry II'
console.log(makeKing());                // 'King Henry III'

函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。

箭头函数同样也可以这样使用默认参数,只不过在只有一个参数时,就必须使用括号而不能省略了:

let makeKing = (name = 'Henry') => `King ${name}`;

console.log(makeKing()); // King Henry

默认参数作用域与暂时性死区 

因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。来看下面的例子:

function makeKing(name = 'Henry', numerals = 'VIII') {
  return `King ${name} ${numerals}`;
}

console.log(makeKing()); // King Henry VIII

这里的默认参数会按照定义它们的顺序依次被初始化。可以依照如下示例想象一下这个过程:

function makeKing() {
  let name = 'Henry';
  let numerals = 'VIII';

  return `King ${name} ${numerals}`;
}

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。看下面这个例子:

function makeKing(name = 'Henry', numerals = name) {
  return `King ${name} ${numerals}`;
}

console.log(makeKing()); // King Henry Henry

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误:

// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') {
  return `King ${name} ${numerals}`;
}

参数也存在于自己的作用域中,它们不能引用函数体的作用域:

// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII';
  return `King ${name} ${numerals}`;
}

参数扩展与收集

ECMAScript 6 新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。

扩展参数

在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。假设有如下函数定义,它会将所有传入的参数累加起来:

let values = [1, 2, 3, 4];

function getSum() {
   let sum = 0;
   for (let i = 0; i < arguments.length; ++i) {
     sum += arguments[i];
   }
    return sum;
 }

这个函数希望将所有加数逐个传进来,然后通过迭代 arguments 对象来实现累加。如果不使用扩展操作符,想把定义在这个函数这面的数组拆分,那么就得求助于 apply() 方法:

console.log(getSum.apply(null, values)); // 10

但在 ECMAScript 6 中,可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。

比如,使用扩展操作符可以将前面例子中的数组像这样直接传给函数:

console.log(getSum(...values)); // 10

因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:

console.log(getSum(-1, ...values));          // 9
console.log(getSum(...values, 5));           // 15
console.log(getSum(-1, ...values, 5));       // 14
console.log(getSum(...values, ...[5,6,7]));  // 28

对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值:

let values = [1,2,3,4]

function countArguments() {
  console.log(arguments.length);
}

countArguments(-1, ...values);          // 5
countArguments(...values, 5);           // 5
countArguments(-1, ...values, 5);       // 6
countArguments(...values, ...[5,6,7]);  // 7

arguments 对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数:

function getProduct(a, b, c = 1) {
  return a * b * c;
}

let getSum = (a, b, c = 0) => {
  return a + b + c;
}

console.log(getProduct(...[1,2]));      // 2
console.log(getProduct(...[1,2,3]));    // 6
console.log(getProduct(...[1,2,3,4]));  // 6

console.log(getSum(...[0,1]));          // 1
console.log(getSum(...[0,1,2]));        // 3
console.log(getSum(...[0,1,2,3]));      // 3

收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似 arguments 对象的构造机制,只不过收集参数的结果会得到一个Array实例。

function getSum(...values) {
  // 顺序累加values中的所有值
 // 初始值的总和为0
  return values.reduce((x, y) => x + y, 0);
}

console.log(getSum(1,2,3)); // 6

收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数:

// 不可以
function getProduct(...values, lastValue) {}

// 可以
function ignoreFirst(firstValue, ...values) {
  console.log(values);
}

ignoreFirst();       // []
ignoreFirst(1);      // []
ignoreFirst(1,2);    // [2]
ignoreFirst(1,2,3);  // [2, 3]

箭头函数虽然不支持arguments对象,但支持收集参数的定义方式,因此也可以实现与使用 arguments 一样的逻辑:

let getSum = (...values) => {
  return values.reduce((x, y) => x + y, 0);
}

console.log(getSum(1,2,3)); // 6

另外,使用收集参数并不影响 arguments 对象,它仍然反映调用时传给函数的参数:

function getSum(...values) {
  console.log(arguments.length);  // 3
  console.log(arguments);         // [1, 2, 3]
  console.log(values);            // [1, 2, 3]
}

console.log(getSum(1,2,3));

函数声明与函数表达式

本文到现在一直没有把函数声明和函数表达式区分得很清楚。事实上,JavaScript 引擎在加载数据时对它们是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。来看下面的例子:

// 没问题
console.log(sum(10, 10));
  function sum(num1, num2) {
    return num1 + num2;
}

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:

// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
  return num1 + num2;
};

上面的代码之所以会出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义,所以上面的代码会出错。这并不是因为使用let而导致的,使用var关键字也会碰到同样的问题:

console.log(sum(10, 10));
var sum = function(num1, num2) {
  return num1 + num2;
};

除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。

注意 在使用函数表达式初始化变量时,也可以给函数一个名称,比如let sum = function sum() {}。

函数作为值

因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。来看下面的例子:

function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument);
}

这个函数接收两个参数。第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值。任何函数都可以像下面这样作为参数传递:

function add10(num) {
  return num + 10;
}

let result1 = callSomeFunction(add10, 10);
console.log(result1);  // 20

function getGreeting(name) {
  return "Hello, " + name;
}

let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2);  // "Hello, Nicholas"

callSomeFunction() 函数是通用的,第一个参数传入的是什么函数都可以,而且它始终返回调用作为第一个参数传入的函数的结果。要注意的是,如果是访问函数而不是调用函数,那就必须不带括号,所以传给 callSomeFunction() 的必须是 add10 和 getGreeting,而不能是它们的执行结果。

从一个函数中返回另一个函数也是可以的,而且非常有用。例如,假设有一个包含对象的数组,而我们想按照任意对象属性对数组进行排序。为此,可以定义一个 sort() 方法需要的比较函数,它接收两个参数,即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。这个问题可以通过定义一个根据属性名来创建比较函数的函数来解决。比如:

function createComparisonFunction(propertyName) {
  return function(object1, object2) {
    let value1 = object1[propertyName];
    let value2 = object2[propertyName];

    if (value1 < value2) {
      return -1;
    } else if (value1 > value2) {
      return 1;
    } else {
      return 0;
    }
  };
}

这个函数的语法乍一看比较复杂,但实际上就是在一个函数中返回另一个函数,注意那个 return 操作符。内部函数可以访问 propertyName 参数,并通过中括号语法取得要比较的对象的相应属性值。取得属性值以后,再按照 sort() 方法的需要返回比较值就行了。这个函数可以像下面这样使用:

let data = [
  {name: "Zachary", age: 28},
  {name: "Nicholas", age: 29}
];

data.sort(createComparisonFunction("name"));
console.log(data[0].name);  // Nicholas

data.sort(createComparisonFunction("age"));
console.log(data[0].name);  // Zachary

在上面的代码中,数组 data 中包含两个结构相同的对象。每个对象都有一个 name 属性和一个 age 属性。默认情况下,sort() 方法要对这两个对象执行 toString(),然后再决定它们的顺序,但这样得不到有意义的结果。而通过调用 createComparisonFunction("name") 来创建一个比较函数,就可以根据每个对象 name 属性的值来排序,结果 name 属性值为 "Nicholas"、age 属性值为 29 的对象会排在前面。而调用 createComparisonFunction("age") 则会创建一个根据每个对象 age 属性的值来排序的比较函数,结果 name 属性值为 "Zachary"、age 属性值为 28 的对象会排在前面。

怎么样?看到这里是不是觉得文章内容似曾相识。不知是否为你解惑一二。

看过的读者应该已经猜到了,本文内容正是来自“ JavaScript 红宝书”第10章,更多前端相关内容,小伙伴可以继续翻阅红宝书查缺补漏哦!

世界读书日,红宝书终终终终于5折啦!等等党,可以剁手了!

☟☟☟ 红宝书全年最低,只要64.5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://nwjs.net/news/141859.html

如若内容造成侵权/违法违规/事实不符,请联系七分地网进行投诉反馈,一经查实,立即删除!

相关文章

SCAN Learning to Classify Images without Labels(翻译)

SCAN Learning to Classify Images without Labels 概览 Approach: A two-step approach where feature learning and clustering are decoupled. Step 1: Solve a pretext task Mine k nearest neighbrs 通过利用特征相似性来挖掘每张图片的最近邻居nearest neighbors&#…

MyEclipse图表工具Birt的使用技巧(三)--连接webservice数据源

Web Services 技术是一套标准。它定义了应用程序怎样在Web上实现互操作。用户能够使用不论什么语言。在不同的平台下编写Web Services。然后通过Web Services 的标准来对这些服务进行注冊、查询和訪问。BIRT能够方便的帮助用户訪问基于SOAP的Web Services&#xff0c;而且使用S…

2020年ACM Fellows出炉!颜水成、周昆、陈怡然等12位华人当选

文&#xff1a;梦佳、贾伟智源社区消息&#xff1a;北京时间1 月14日&#xff0c;国际计算机协会&#xff08;ACM&#xff09;宣布了 2020 年新当选 ACM Fellow 名单&#xff0c;共有95位科学家当选&#xff0c;其中包括12位华人&#xff0c;智源学者颜水成入选。ACM 创立于 19…

新书上市 | 豆瓣评分8.4,美国数学学会推荐必读经典,重版再现!

彼得森&#xff08;Ivars Peterson&#xff09;在《当代数学研讨》一文中曾说&#xff1a;“对大多数外行人来说&#xff0c;现代数学是一块陌生的领地……数学是一个值得探索的世界……但可悲的是&#xff0c;外行人进入这一世界的道路似乎太少了。”代数学作为数学中的重要分…

多核之后,CPU 的发展方向是什么?中科院计算所包云岗提 20 点新思考

作者&#xff1a;包云岗在知乎上有一个问题&#xff1a;多核之后&#xff0c;CPU 的发展方向是什么&#xff1f;中科院计算所研究员、智源学者包云岗针对该问题进行了详细回答。智源社区将包云岗研究员的回答内容转载过来&#xff0c;供读者参考。包云岗&#xff0c;中国科学院…

安装Ubuntu13.10后必做的10件事

Ubuntu 13.10发布了&#xff0c;而且你已经升级了&#xff0c;然后你想知道现在要做些什么。不要着急&#xff0c;这里有10件安装完Ubuntu 13.10后必做的事。 我们以前为ubuntu每个版本整理了一个安装后核对表&#xff0c;但是因为新功能的到来以及不断的进步&#xff0c;我们建…

从对比学习(Contrastive Learning)到对比聚类(Contrastive Clustering)

从对比学习(Contrastive Learning)到对比聚类(Contrastive Clustering) 作者&#xff1a;凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 想要了解对比聚类&#xff0c;首先应该清楚对比学习的整个过程。最经典的对比学习的文章是Hinton团队提出的SimCLR&#xff0c;该…

演讲实录 | DevOps 2021年度洞察

2021DevOpsDays上海站闪耀落幕4月17日的DevOpsDays峰会上海站圆满落幕&#xff0c;众多业内大咖及风云人物悉数亮相&#xff0c;为大家分享了DevOps、精益、敏捷的最新落地实践及案例。全量PPT下载&#xff0c;提取码&#xff1a;3fshhttps://pan.baidu.com/share/init?surlVb…

2021年, 别再只沉迷于GANs 和 Transformer,GNN爆发已经从CV蔓延到物理化学

作者&#xff1a;Sergei Ivanov编译&#xff1a;周寅张皓、梦佳、贾伟GNN&#xff0c;又可以被理解为Neural Networks for Graph&#xff0c;由于图非欧结构的限制&#xff0c;如何设计图数据上的神经网络一直困扰着学界&#xff0c;因此在数年前该领域一直较为沉寂。但近年来&…

新书上市 | 学校没有教的科学写作指南

日常生活中常有这样的情况&#xff1a;有的人埋头工作&#xff0c;但不擅长总结写报告&#xff0c;工作进展和成果无法传达给他人&#xff1b;有的人长于讨论&#xff0c;会议上很活跃&#xff0c;但写不好讨论纪要&#xff0c;无法将讨论的成果积累下来。写作本应是帮助人脑表…

智能,万亿维空间中的求解

作者&#xff1a;Terry J. Sejnowski编译&#xff1a;贾伟、梦佳1884年&#xff0c;Edwin Abbott 在讽刺小说《平面国》中描述了这样一个世界&#xff0c;这个国家生活在一个二维世界中&#xff0c;平面国的人们只能够理解二维数学&#xff1b;但其中一个方块绅士做了一个关于球…

新书上市 | 世界名校数据挖掘经典《斯坦福数据挖掘教程(第3版)》

题图 | 作者为 Scott Ullman《斯坦福数据挖掘教程&#xff08;第3版&#xff09;》上架之后&#xff0c;这是我们第一次整篇文章介绍这本书。这本书相当受欢迎&#xff08;前两个版本累计销量超过 5 万册&#xff09;&#xff0c;尤其是受学校青睐——在此也说声抱歉&#xff…

智源唐杰主编的IEEE Transactions on Big Data期刊被SCI收录 | AI日报

智源唐杰主编的IEEE Transactions on Big Data期刊被SCI收录今日&#xff0c;IEEE Transactions on Big Data (简称&#xff1a;IEEE TBD&#xff09;被SCI收录。IEEE TBD成立背景&#xff1a;随着人类社会进入数字化时代&#xff0c;产生的数据也在爆发式增长&#xff0c;这些…

Python 工匠:在边界处思考

这是 “Python 工匠”系列的第 15 篇文章。[点击原文链接查看所有]2016 年&#xff0c;Linux 操作系统的创造者 Linus Torvalds 参加了一场 TED 访谈节目[1]。整个节目的前半部分&#xff0c;主要是他在讲如何在家光着膀子写出 Linux 的故事&#xff0c;没有涉及太多编程相关的…

ImageNet的top-1终于上了90%,网友质疑:用额外数据集还不公开,让人怎么信服?...

转载自&#xff1a;机器之心近日&#xff0c;谷歌大脑研究科学家、AutoML 鼻祖 Quoc Le 发文表示&#xff0c;他们提出了一种新的半监督学习方法&#xff0c;可以将模型在 ImageNet 上的 top-1 准确率提升到 90.2%&#xff0c;与之前的 SOTA 相比实现了 1.6% 的性能提升。这一成…

《Adobe Acrobat DC经典教程》—第1章1.11节在阅读模式下查看PDF文件

本节书摘来自异步社区《Adobe Acrobat DC经典教程》一书中的第1章1.11节在阅读模式下查看PDF文件&#xff0c;作者【美】Lisa Fridsma&#xff08;丽莎 弗里斯玛&#xff09; , Brie Gyncild&#xff08;布里 根希尔德&#xff09;,更多章节内容可以访问云栖社区“异步社区”公…

opencv python 多帧降噪算法_防抖技术 | OpenCV实现视频稳流

在这篇文章中&#xff0c;我们将学习如何使用OpenCV库中的点特征匹配技术来实现一个简单的视频稳定器。我们将讨论算法并且会分享代码(python和C版)&#xff0c;以使用这种方法在OpenCV中设计一个简单的稳定器。 视频中低频摄像机运动的例子 视频防抖是指用于减少摄像机运动对…

被嫌弃的贝叶斯派的逆袭

多数人第一次听说贝叶斯定理应该是在中学课堂上。那个公式看起来并不复杂&#xff0c;在一众花里胡哨的考点中显得平平无奇。但是很快&#xff0c;我们就知道了什么叫深藏不露。从高校教材到研究实战&#xff0c;贝叶斯这三个字频繁地出现在信息科学的重要议题中&#xff0c;它…

ICLR 2021论文接收统计出炉!Top20 机构,国内仅清华在榜!

转载自&#xff1a;AI科技评论ICLR 2021于前些天正式放榜&#xff0c;本次ICLR 2021一共有2997篇有效论文投稿&#xff0c;最后一共860篇被接收&#xff0c;录取率达到了28.7%&#xff0c;相比去年的26.5%有提升&#xff0c;与其他一些AI顶会大幅降低接收率相比&#xff0c;ICL…

为什么 Linux 和 macOS 不需要碎片整理

为什么这么设计&#xff08;Why’s THE Design&#xff09;是一系列关于计算机领域中程序设计决策的文章&#xff0c;我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题&#xff0c;可以在…