「读书笔记」第四版JavaScript高级程序设计(第十章)

869 阅读6分钟

每一个函数都是Function类型的实例,函数是对象,函数名就是指向对象的指针,函数名不一定与函数绑定。

// 需要分号
let sum = (num1, num2) => {
  return num1 + num2;
};
// 不需要分号
function sum (num1, num2) {
  return num1 + num2;
}

箭头函数

箭头函数虽然简洁,但是在一些场景下不适用,箭头函数不能使用 arguments、super 和 new.target,箭头函数不包含prototype属性。

函数名

函数名就是函数的指针。一个函数可以有多个名称

function sum(num1, num2) {
  return num1 + num2;
}
// 20
console.log(sum(10, 10));
// 20
let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20

name属性

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

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

function sum(num1, num2) {
  return num1 + num2;
}
let anotherSum = sum;
// sum
console.log(anotherSum.name);

使用bind后会添加前缀


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


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

参数

js中定义参数的数量,和使用时传入参数不同的情况下,是不会产生错误。因为js中参数的本质是一个数组。在非箭头函数的体内,可以使用arguments对象访问函数的每一个参数,arguments是一个类数组对象。ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的


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

在非严格模式下,参数和arguments是相互绑定的(但是它们指向的并不是同一块内存空间,只是会相互同步),修改arguments[0]的值同时会修改第一个参数的值。

但是如果一个实际的参数都没有传,修改arguments[0]同步。

箭头函数的参数

箭头函数中无法arguments关键字

没有重载

js中没有函数的重载,后定义的会覆盖之前定义的。

默认参数


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

如果参数是undefined,使用的是默认参数。arguments不和默认参数相互同步,参数如果使用了默认参数,arguments是拿不到默认参数的值的。

默认参数可以不是固定的值,也可以是函数的返回值

let ordinality = 0;
function getNumerals() {
  return ordinality++;
}
// 使用了getNumerals的返回值,作为默认参数
function makeKing(name = 'Henry', numerals = getNumerals()) {
  return `King ${name} ${numerals}`;
}
// numerals的默认参数。在每次调用后,会递增
console.log(makeKing());

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

函数的默认参数和let和const一样,存在暂时性死区。后一个的参数的默认值,可以使用前一个参数当作默认值。


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

但是前一个参数,不能使用后一个参数当作默认值。具体原因如下


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

// 类似等价于
function makeKing(name, numerals) {
  let name = name || 'Henry';
  let numerals = numerals || 'VIII';
  return `King ${name} ${numerals}`;
}

参数的扩展和收集

如果函数想要接受数组中的每一个内容作为参数。同时又不能接受数组参数

如果不使用扩展运算符,可以使用apply

let values = [1, 2, 3, 4];
getSum.apply(null, values)

或者使用扩展运算符

let values = [1, 2, 3, 4];
getSum(...values);

arguments可以收集到使用扩展运算符传入的参数


let values = [1,2,3,4]
function countArguments() {
  console.log(arguments.length);
}
// 5
countArguments(-1, ...values);

扩展运算符,可以用在命名参数和默认参数上

function getProduct(a, b, c = 1) {
  return a * b * c;
}
// 2
console.log(getProduct(...[1,2]));
// 6
console.log(getProduct(...[1,2,3]))

收集参数

扩展运算符也可以用在函数的形参上,参数会变成一个数组

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

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

如果使用扩展运算符用作参数上,那么它之后不能在有参数了,因为它的长度是可变的

// 错误
function getProduct(...values, lastValue) {}
// 正确
function getProduct(lastValue, ...values) {}

函数的声明和表达式

函数声明和函数表达式,js引擎是区别对待的。

js在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

// 能够正常执行
console.log(sum(10, 10));
function sum(num1, num2) {
  return num1 + num2;
}

函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)

在执行代码时js引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。

// 你看到是这样的
console.log(sum(10, 10));
function sum(num1, num2) {
  return num1 + num2;
}

// 在js内部,由于函数声明提升,它其实是这样的
function sum(num1, num2) {
  return num1 + num2;
}
console.log(sum(10, 10));

函数表达式不存在函数声明提升

// 会报错。代码如果没有执行到定义的那一行,那么执行上下文中就没有函数的定义
console.log(sum(10, 10));
var sum = function(num1, num2) {
  return num1 + num2;
};

函数作为值

函数可以作为参数,可以作为返回值。作为返回值的函数,可以外部的函数的内容。

函数内部

this

在标准函数中,this是把函数当成方法调用的上下文对象。标准函数的this对象,只有它被调用的时候,才会确定是什么, 在没有调用的时候,this是不确定的。

window.color = 'red';
let o = {
  color: 'blue'
};
function sayColor() {
  // this, 指向的是window
  console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
// this指向的是o对象
o.sayColor(); // 'blue'

箭头函数中,this引用的是定义箭头函数的上下文(如果此时的上下文)

window.color = 'red';
let o = {
  color: 'blue'
};
// this始终是window对象
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'

箭头函数中,this引用的是定义箭头函数的上下文。如果箭头函数的上下文在普通函数的内部,那么上下文需要等到普通函数调用才能确定。

const bar = {
  fn () {
    const fn = () => {
      console.log(this)
    }
    fn()
  }
}
// this是bar
bar.fn()
const fn = bar.fn
// this是window
fn()
const bar = {
  fn: () => {
    // 这个时候的this,在定义时就已经确定了
    console.log(this)
  }
}
// this是window
// 在fn函数定义时就已经确定了
bar.fn()

caller

调用当前函数的函数, arguments.callee.caller也指向同一个引用(但是目前测试,返回的是undefined)。


function a() {
  b();
}
function b() {
  // a
  console.log(b.caller);
}
a();

arguments

类数组对象。包含了调用函数时,实际传入的所有参数。arguments.callee 指向所在函数的指针。

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1); }
}
// 等价于 ,避免和factorial函数名字强耦合
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

new.target

函数是普通调用new.target返回undefined,如果通过new调用返回构造函数

函数的属性和方法

  • length, length返回函数形参的个数
  • prototype,是保存引用类型所有实例方法的地方,所有实例共享
  • apply(), 用于指定this,第一个参数是需要设置的this值,第二个参数可以是参数数组或者arguments对象
  • call(), 用于指定this,第一个参数和apply,但是函数的参数需要依次传入,不能使用数组
  • bind(), 用于指定this, 参数也需要依次传入,与apply,call立即调用原函数不同,bind调用后会返回包装后的函数
  • valueOf(), 返回函数本身

偏函数

bind,另外常用的方法就是让函数有一些预设的参数

function addArguments(arg1, arg2) {
  return arg1 + arg2
}

// add初始参数有1
const add = addArguments.bind(null, 1)
// 3
add(2)
// 3, 第二个参数被无视
add(2, 3)

函数表达式

函数表达式和函数声明最注要的区别就是提升

// 这段代码存在问题
if (toggle) {
  function a () {
    console.log(1)
  }
} else {
  function a () {
    console.log(2)
  }
}

// 在js引擎中,它其实张这样
function a () {
  console.log(1)
}
function a () {
  console.log(2)
}
if (toggle) {
} else {
}

但是如果是使用函数表达式,这样做是安全的


let sayHi;
if (condition) {
  sayHi = function() {
    console.log("Hi!");
  };
} else { 10
  sayHi = function() {
    console.log("Yo!");
  };
}

递归

一个函数调用自己,如果通过函数名递归,这个函数会被强绑定。可以使用arguments.callee获得自身的引用。不过严格模式下使用arguments.callee会报错。

闭包

闭包指的是那些引用了另一个函数作用域中变量的函数


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

createComparisonFunction内部函数被返回后,依然可以使用propertyName这个变量,因为内部函数依然保留createComparisonFunction函数的作用域。

内部匿名函数的作用域列表包含了匿名函数的AO,以及外部createComparisonFunction函数的AO,以及全局的VO。createComparisonFunction返回匿名函数后,由于匿名函数包含了createComparisonFunction函数的AO,所以依然能访问到createComparisonFunction中的变量。副作用就是,createComparisonFunction的AO无法被回收。♻️

--------分割线-------

闭包的内容红宝书在这里介绍不如汤姆大叔的博客详细,这里就不在赘述了。这里推荐看下汤姆大叔的博客,博客非常的赞💗💗💗💗

👍👍👍👍👍👍👍博客地址: https://www.cnblogs.com/tomxu/archive/2012/01/18/2312463.html,如果想看精简的内容,可以看我的「学习笔记」深入理解JavaScript:https://github.com/peoplesing1832/blog/issues/43

--------分割线-------

需要注意的是闭包会保留其他函数的作用域,过度使用闭包可能会导致内存溢出,有时我们需要手动的释放内存

function foo () {
  return function () {
  }
}
let bar = foo()
bar()
// 手动释放内存
bar = null

this

因为内部函数是无法访问外部函数的this对象,所以这里的this是window

window.identity = 'The Window';
  let object = {
    identity: 'My Object',
    getIdentityFunc() {
      return function() { return this.identity;
    };
  }
};

// window, 
console.log(object.getIdentityFunc()());

但是如果把外部函数的this保存到一个变量里,可以通过闭包,实现获取外部的this


window.identity = 'The Window';
let object = {
  identity: 'My Object',
  getIdentityFunc() {
    let that = this;
    return function() {
      return that.identity;
    };
  }
};
console.log(object.getIdentityFunc()()); // 'My Object'

内存泄漏

function assignHandler() {
  let element = document.getElementById('someElement'); 
  element.onclick = () => console.log(element.id);
}

事件处理函数,使用了assignHandler的AO(活动对象),导致element变量不能被回收。如何避免内存泄漏呢


function assignHandler() {
  let element = document.getElementById('someElement');
  let id = element.id;
  element.onclick = () => console.log(id);
  element = null;
}

将element.id保存到一个变量,但是事件处理函数还是使用了assignHandler的AO,element变量不能被回收。必须把element设置为null,才能接触对element的引用。

立即调用的函数表达式

使用IIFE可以模拟块级作用域


// IIFE
(function () {
  for (var i = 0; i < count; i++) {
    console.log(i);
  }
})();
console.log(i); // 抛出错误

在es6,中let和const就是块级作用域变量,无需使用IIFE