MDN速刷 JS JS指南篇——函数

开始之前

听说,函数是 JS 中的一等公民

定义

原始类型作为参数传入函数没有关系,引用类型作为参数传入,传入的类似于 C 语言指针,会改变指针指向的地址内的值。

function myFunc(theObject) {
  theObject.make = 'Toyota';
}

var mycar = { make: 'Honda', model: 'Accord', year: 1998 };
var x, y;

x = mycar.make; // x获取的值为 "Honda"

myFunc(mycar);

y = mycar.make; // y获取的值为 "Toyota"
// (make属性被函数改变了)

函数表达式

const square = function (number) {
  return number * number;
};
var x = square(4); // x gets the value 16
const factorial = function fac(n) {
  return n < 2 ? 1 : n * fac(n - 1);
};

console.log(factorial(3));

将函数作为参数传递给另一个函数时,函数表达式很方便。

function map(f, a) {
  let result = []; // 创建一个数组
  let i; // 声明一个值,用来循环
  for (i = 0; i != a.length; i++) result[i] = f(a[i]);
  return result;
}
const f = function (x) {
  return x * x * x;
};
let numbers = [0, 1, 2, 5, 10];
let cube = map(f, numbers);
console.log(cube);

条件定义

var myFunc;
if (num == 0) {
  myFunc = function (theObject) {
    theObject.make = 'Toyota';
  };
}
new Function('a', 'b', 'return a + b');

调用

每个 JavaScript 函数实际上都是一个 Function 对象。

(function () {}.constructor === Function); // true

实例方法有 bind(), call(), apply(), toString();

var x = 10;

function createFunction1() {
  var x = 20;
  return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}

function createFunction2() {
  var x = 20;
  function f() {
    return x; // 这里的 x 指向上方本地作用域内的 x
  }
  return f;
}

var f1 = createFunction1();
console.log(f1()); // 10
var f2 = createFunction2();
console.log(f2()); // 20

函数作用域

在函数内定义的变量不能在函数之外的任何地方访问,一个函数可以访问定义在其范围内的任何变量和函数。

作用域和函数堆栈

递归

一个函数可以指向并调用自身。有三种方法可以达到这个目的:

  1. 函数名
  2. arguments.callee
  3. 作用域下的一个指向该函数的变量名
var foo = function bar() {
  // statements go here
};

// 在这个函数体内,以下的语句是等价的:
bar();
arguments.callee();
foo();

// 递归需要一个终止条件

function loop(x) {
  if (x >= 10)
    // "x >= 10" 是退出条件(等同于 "!(x < 10)")
    return;
  // 做些什么
  loop(x + 1); // 递归调用
}
loop(0);

//有些算法并不能简单的用迭代来实现。例如,获取树结构中所有的节点时,使用递归实现要容易得多:
function walkTree(node) {
  if (node == null)
    //
    return;
  // do something with node
  for (var i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}

堆栈

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(3);

// 输出:

// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3
// 可以看到压栈和弹出的顺序

嵌套函数和闭包

可以在一个函数里面嵌套另外一个函数

嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。

既然嵌套函数是一个闭包,就意味着一个嵌套函数可以”继承“容器函数的参数和变量。换句话说,内部函数包含外部函数的作用域。

  • 内部函数只可以在外部函数中访问。
  • 内部函数形成了一个闭包:它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。
function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41

由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数:

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加3
result = fn_inside(5); // returns 8

result1 = outside(3)(5); // returns 8

保存变量

一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 inside 没有再被引用时,内存才会被释放。

这与在其他对象中存储引用没什么不同,但是通常不太明显,因为并不能直接设置引用,也不能检查它们。

多层嵌套函数

函数可以被多层嵌套,故而形成多个闭包,因此闭包可以包含多个作用域。

多层闭包递归包含了所有包含它的函数作用域,这个被称为作用域链。

命名冲突

当同一个闭包作用域下两个参数或变量同名,就会产生命名冲突。更近的作用域有更高的优先级。

遵循就近原则。

闭包

闭包是 JavaScript 中最强大的特性之一。

由于内部函数可以访问外部函数的作用域,因此当内部函数生命周期大于外部函数时,外部函数中定义的变量和函数的生命周期将比内部函数执行时间长。当内部函数以某种方式被任何一个外部作用域访问时,一个闭包就产生了。

var createPet = function (name) {
  var sex;

  return {
    setName: function (newName) {
      name = newName;
    },

    getName: function () {
      return name;
    },

    getSex: function () {
      return sex;
    },

    setSex: function (newSex) {
      if (
        typeof newSex == 'string' &&
        (newSex.toLowerCase() == 'male' || newSex.toLowerCase() == 'female')
      ) {
        sex = newSex;
      }
    },
  };
};

var pet = createPet('Vivie');
pet.getName(); // Vivie

pet.setName('Oliver');
pet.setSex('male');
pet.getSex(); // male
pet.getName(); // Oliver

上述代码中,外部函数 name 变量对内嵌函数来说是可见的,而除了通过内嵌函数本身,没有其他任何方法可以取得内嵌的变量。内嵌函数的内嵌变量就像内嵌函数的保险柜。它们会为内嵌函数保留“稳定”而又安全的数据参与运行。

最后,闭包要避免命名冲突。

arguments

函数实参会被保存在一个类似数组的 arguments 对象中。在函数内,可以访问 aguments 来获取实参。

使用 arguments 就使得函数参数可变。

function myConcat(separator) {
  var result = ''; // 把值初始化成一个字符串,这样就可以用来保存字符串了!!
  var i;
  // iterate through arguments
  for (i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}

arguments 只是一个类似于数组的对象,而不是数组,类似数组是指它有索引编号和.length 属性。

函数参数

函数参数默认值是 undefined

函数默认参数可修改。

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

multiply(5); // 5

剩余参数

剩余参数语法允许将不确定数量的参数表示为数组。

function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

箭头函数

相对于函数有较短的语法并以词法的方式绑定 this。

在箭头函数出现之前,每一个新函数都重新定义了自己的 this 值。

箭头函数捕捉闭包上下文的 this 值。

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // 这里的`this`正确地指向person对象
  }, 1000);
}

var p = new Person();

预定义函数

eval()

eval()方法会对一串字符串形式的 JavaScript 代码字符求值。

uneval()

uneval()方法创建的一个 Object 的源代码的字符串表示。

isFinite()

isFinite()函数判断传入的值是否是有限的数值。

如果需要的话,其参数首先被转换为一个数值。

isNaN()

isNaN()函数判断一个值是否是 NaN。

注意:isNaN 函数内部的强制转换规则十分有趣;

另一个可供选择的是 ECMAScript 6 中定义 Number.isNaN() , 或者使用 typeof 来判断数值类型。

parseFloat()

parseFloat() 函数解析字符串参数,并返回一个浮点数。

parseInt()

parseInt() 函数解析字符串参数,并返回指定的基数(基础数学中的数制)的整数。

decodeURI()

decodeURI() 函数对先前经过 encodeURI 函数或者其他类似方法编码过的字符串进行解码。

decodeURIComponent()

decodeURIComponent()方法对先前经过 encodeURIComponent 函数或者其他类似方法编码过的字符串进行解码。

encodeURI()

encodeURI()方法通过用以一个,两个,三个或四个转义序列表示字符的 UTF-8 编码替换统一资源标识符(URI)的某些字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。

encodeURIComponent()

encodeURIComponent() 方法通过用以一个,两个,三个或四个转义序列表示字符的 UTF-8 编码替换统一资源标识符(URI)的每个字符来进行编码(每个字符对应四个转义序列,这四个序列组了两个”替代“字符)。

escape()

已废弃的 escape() 方法计算生成一个新的字符串,其中的某些字符已被替换为十六进制转义序列。使用 encodeURI 或者 encodeURIComponent 替代本方法。

unescape()

已废弃的 unescape() 方法计算生成一个新的字符串,其中的十六进制转义序列将被其表示的字符替换。上述的转义序列就像 escape 里介绍的一样。因为 unescape 已经废弃,建议使用 decodeURI()或者 decodeURIComponent 替代本方法。

小结

多层闭包递归包含了所有包含它的函数作用域,这个被称为作用域链。

闭包要注意命名冲突

箭头函数没有自己的 this,所以会捕获上下文的 this

突然想起来面试时,有个问题忘了记录

前端内存泄漏一般出现在什么情况下

意外的全局变量

严格模式可避免

function foo() {
  bar = 2;
  console.log('bar没有被声明!');
}

dom 清空时,还存在引用

很多时候,为了方便存取,经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该 DOM 节点时,忘记解除对它的引用,则会造成内存泄露.

var element = {
  shotCat: document.getElementById('shotCat'),
};

document.body.removeChild(document.getElementById('shotCat'));
// 如果element没有被回收,这里移除了 shotCat 节点也是没用的,shotCat 节点依然留存在内存中.

与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的

定时器中的内存泄漏

var someResource = getData();
setInterval(function () {
  var node = document.getElementById('Node');
  if (node) {
    node.innerHTML = JSON.stringify(someResource);
  }
}, 1000);

如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 但是 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.

不规范地使用闭包

function foo() {
  var a = {};
  function bar() {
    console.log(a);
  }
  a.fn = bar;
  return bar;
}

bar 和 a 形成了相互循环引用。bar 作为一个闭包,即使它内部什么都没有,foo 中的所有变量都还是隐式地被 bar 所引用。

即使 bar 内什么都没有还是造成了循环引用,那真正的解决办法就是,不要将 a.fn = bar.

评论0

  • 还没有评论,来说点什么吧。