js学习笔记(3)

55 阅读7分钟
1.函数属性与方法

每个函数都有两个属性:length和prototyp。length属性保存函数定义的命名参数的个数。

function sayName(name) {
  console.log(name);
}
function sum(num1, num2) {
  return num1 + num2;
}
function sayHi() {
  console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0

prototype 是保存引用类型所有实例方法的地方,这意味着toString()、valueOf()等方法实际上都保存在prototype 上,进而由所有实例共享。prototype 属性是不可枚举的,因此使用for-in 循环不会返回这个属性。

2.apply(),call(),bind()

这两个方法都会以指定的this 值来调用函数,即会设置调用函数时函数体内this 对象的值
apply()方法接收两个参数:函数内this 的值和一个参数数组。第二个参数可以是Array 的实例,但也可以是arguments 对象。

function sum(num1, num2) {
  return num1 + num2;
}
function callSum1(num1, num2) {
  return sum.apply(this, arguments); // 传入arguments 对象
}
function callSum2(num1, num2) {
  return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20

在这个例子中,callSum1()会调用sum()函数,将this 作为函数体内的this 值(这里等于window,因为是在全局作用域中调用的)传入,同时还传入了arguments 对象。callSum2()也会调用sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。

call()方法与apply()的作用一样,只是传参的形式不同。第一个参数跟apply()一样,也是this 值,而剩下的要传给被调用函数的参数则是逐个传递的,通过call()向函数传参时,必须 将参数一个一个地列出来

function sum(num1, num2) {
  return num1 + num2;
}
function callSum(num1, num2) {
  return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20

严格模式下,调用函数时如果没有指定上下文对象,则this 值不会指向window。 除非使用apply()或call()把函数指定给一个对象,否则this 的值会变成undefined。

到底是使用apply()还是call(),完全取决于怎么给要调用的函数传参更方便。如果想直接传arguments 对象或者一个数组,那就用apply();否则,就用call()。
注:控制函数调用上下文即函数体内this值的能力如下示例

window.color = 'red';
let o = {
  color: 'blue'
};
function sayColor() {
  console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue

sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示"red"。这是因为this.color 会求值为window.color。
如果在全局作用域中显式调用sayColor.call(this)或者sayColor.call(window),则同样都会显示"red"。而在使用sayColor.call(o)把函数的执行上下文即this 切换为对象o 之后,结果就变成了显示"blue"了。
bind()方法会创建一个新的函数实例,其this 值会被绑定到传给bind()的对象

window.color = 'red';
var o = {
  color: 'blue'
};
function sayColor() {
  console.log(this.color);
}
let objectSayColor = sayColor.

sayColor()上调用bind()并传入对象o 创建了一个新函数objectSayColor()。 objectSayColor()中的this 值被设置为o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串"blue"。

3.函数表达式

函数声明提升
即函数声明会在代码执行之前获得定义。这意味着函数声明 可以出现在调用它的代码之后:

sayHi();
function sayHi() {
  console.log("Hi!");
}

这个例子不会抛出错误,因为JavaScript 引擎会先读取函数声明,然后再执行代码。
但是,函数表达式跟JavaScript 中的其他表达式一样,需要先赋值再使用。下面的例子会导致错误:

sayHi(); // Error! function doesn't exist yet
let sayHi = function() {
  console.log("Hi!");
};

函数表达式有几种不同的形式

let functionName = function(arg0, arg1, arg2) {
// 函数体
};

函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName.
注意,不要这么写

// 千万别这样做!
if (condition) {
  function sayHi() {
    console.log('Hi!');
  }
} else {
  function sayHi() {
    console.log('Yo!');
  }
}

事实上,这种写法在ECAMScript 中不是有效的语法。JavaScript 引擎会尝试将其纠正为适 当的声明。问题在于浏览器纠正这个问题的方式并不一致
应该这么写

// 没问题
let sayHi;
if (condition) {
  sayHi = function() {
    console.log("Hi!");
  };
} else {
  sayHi = function() {
    nsole.log("Yo!");
  };
}

4.递归
递归函数通常的形式是一个函数通过名称调用自己

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

5.尾调用优化
ECMAScript 6 规范新增了一项内存管理优化机制,让JavaScript 引擎在满足条件时可以重用栈帧。 具体来说,这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。

尾调用优化的条件就是确定外部栈帧真的没有必要存在了。
1.代码在严格模式下执行;
2.外部函数的返回值是对尾调用函数的调用;
3.尾调用函数返回后不需要执行额外的逻辑;
4.调用函数不是引用外部函数作用域中自由变量的闭包。

下面展示了几个违反上述条件的函数,因此都不符号尾调用优化的要求:

"use strict";
// 无优化:尾调用没有返回
function outerFunction() {
  innerFunction();
}
// 无优化:尾调用没有直接返回
function outerFunction() {
  let innerFunctionResult = innerFunction();
  return innerFunctionResult;
}
// 无优化:尾调用返回后必须转型为字符串
function outerFunction() {
  return innerFunction().toString();
}
// 无优化:尾调用是一个闭包
function outerFunction() {
  let foo = 'bar';
  function innerFunction() { return foo; }
  return innerFunction();
}

下面是几个符合尾调用优化条件的例子:

"use strict";
// 有优化:栈帧销毁前执行参数计算
function outerFunction(a, b) {
  return innerFunction(a + b);
}
// 有优化:初始返回值不涉及栈帧
function outerFunction(a, b) {
  if (a < b) {
    return a;
  }
  return innerFunction(a + b);
}
// 有优化:两个内部函数都在尾部
function outerFunction(condition) {
  return condition ? innerFunctionA() : innerFunctionB();
}

第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多 少次嵌套函数,都只有一个栈帧

另外尾调用优化的代码例子,通过递归计算斐波纳契数列的函数

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

改为函数执行递归:

"use strict";
// 基础框架
function fib(n) {
  return fibImpl(0, 1, n);
}
// 执行递归
function fibImpl(a, b, n) {
  if (n === 0) {
    return a;
}
  return fibImpl(b, a + b, n - 1);
}
6.闭包

在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。
调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。
作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局 部活动对象会被销毁,内存中就只剩下全局作用域。
闭包就不一样了。在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。

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;
    }
  };
}

function compare(value1, value2) {
  if (value1 < value2) {
    return -1;
  } else if (value1 > value2) {
    return 1;
  } else {
    return 0;
  }
}
let result = compare(5, 10);

因此,在createComparisonFunction()函数中,匿名函数的作用域链中实际上包含createComparison- Function()的活动对象。 在create-ComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留 在内存中,直到匿名函数被销毁后才会被销毁:

// 创建比较函数
let compareNames = createComparisonFunction('name');
// 调用函数
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' });
// 解除对函数的引用,这样就这样就可以释放内存了
compareNames = null;