每一个函数都是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