代理
代理的运用场景
跟踪属性访问
根据对基本操作 get set has等的捕获, 我们可以知道对象属性什么时候被访问、被查询
const user = {
name: 'Jake'
}
const proxy = new Proxy(user, {
get(target, p, receiver) {
console.log(`Getting ${p}`);
return Reflect.get(...arguments);
},
set(target, p, value, receiver) {
console.log(`Setting ${p}=${value}`);
return Reflect.set(...arguments);
}
});
console.log(proxy.name)
proxy.age = 22
隐藏属性(跳过属性)
学习了那么多捕捉器, 我们足以实现隐藏某些属性的功能
const hiddenProperty = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
let proxy = new Proxy(targetObject, {
// get?(target: T, p: string | symbol, receiver: any): any;
get(target, property, receiver) {
if (hiddenProperty.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
// has?(target: T, p: string | symbol): boolean;
has(target, property) {
if (hiddenProperty.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
console.log(proxy.foo) // undefined
console.log(proxy.bar) // undefined
console.log(proxy.baz) // 3
console.log('foo' in proxy) // false
console.log('bar' in proxy) // false
console.log('baz' in proxy) // true
属性验证(拦截set捕捉器)
const target = {
onlyNumbersGoHero: 0
};
let proxy = new Proxy(target, {
set(target, p, value, receiver) {
// 验证值的属性是不是 number
if (typeof value != "number") {
return false;
} else {
return Reflect.set(...arguments);
}
}
});
proxy.onlyNumbersGoHero = 1;
console.log(proxy.onlyNumbersGoHero);
proxy.onlyNumbersGoHero = '2';
console.log(proxy.onlyNumbersGoHero);
函数与构造函数参数验证(拦截函数调用和构造函数调用)
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种 类型的值:
function median(...nums) {
// 排序且取中间那个数字
return nums.sort()[Math.floor(nums.length / 2)];
}
let proxy = new Proxy(median, {
apply(target, thisArg, argArray) {
for (let arg of argArray) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided'; // Non-number argument provided
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy(5, 1, 3)); // 3
console.log(proxy(4, 7, '1'));
构造函数参数验证
class User {
constructor(id) {
this.id_ = id;
}
}
// 对 User 构造函数设置代理对象
let proxy = new Proxy(User, {
construct(target, argArray, newTarget) {
if (argArray[0] === undefined) {
throw 'User cannot be instantiated without id';
} else {
console.log('调用了构造函数');
return Reflect.construct(...arguments);
}
}
});
let proxy1 = new proxy(1);
let proxy2 = new proxy();
数据绑定与可观察对象(代理构造函数)
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的 代码互操作。 比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
const userList = [];
class User {
constructor(name) {
this.name_ = name;
}
}
let UserProxy = new Proxy(User, {
construct(target, argArray, newTarget) {
let newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
}
});
new UserProxy('John');
new UserProxy('Jacob');
new UserProxy('Jingleheimerschmidt');
/*
[
User { name_: 'John' },
User { name_: 'Jacob' },
User { name_: 'Jingleheimerschmidt' }
]
*/
console.log(userList);
对集合做代理
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
}
});
proxy.push('John');
// John
// 1 这个是数组的长度
proxy.push('Jacob');
// Jacob
// 2 这个是数组的长度
console.log(proxy);
函数
本章节大概的内容
function sum(num1, num2) {
return num1 + num2;
}
let sum = function(num1, num2) {
return sum1 + sum2;
}
let sum = (num1, num2) => {
return num1 + num2;
}
不推荐使用
let function1 = new Function('num1', 'num2', 'return num1 + num2');
console.log(function1(20, 20))
好了
箭头函数
任何可以使用函数表达式的地方,都可以使用箭头函数
只有一个参数时, 可以不需要参数的小括号
let func = x => {
console.log(x);
}
但是没有参数则需要添加括号
let func () => {
console.log("hello world");
}
有些时候箭头函数的花扩号是可以省略的
let func = x => console.log(x);
func(10); // 10
let func02 = () => console.log("hello world");
func02(); // hello world
但是这种方式是不能省略花括号的
let func = () => { return 10; }
箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用
arguments、super和new.target,也不能用作构造函数。此外,箭头函数也没有prototype属性。
函数名(对标c语言的函数指针)
因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称
function sum(num1, num2) {
return num1 + num2;
}
let anotherSum = sum;
console.log(anotherSum(10, 10)); // 20
sum = null;
console.log(anotherSum(10, 20)); // 30
注意,使用不带括号的函数名会访问函数指针,而不会执行函数
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
如果bind第一个参数传递的是 null , 则执行该函数作用域内的 this 将代替该参数
绑定函数参数返回新的函数
function fn(a, b, c) {
return a + b + c;
}
let fn1 = fn.bind(null, 10);
console.log(fn1(17, 15)); // 42
上面的代码使得 fn 有了个默认的参数 10 , 它将会被传递给参数
a, 这是bind的其中一个功能
bind 将函数绑定到对象上
this.x = 9;
let module1 = {
x: 81,
getX: function () {
return this.x;
}
}
console.log(module1.getX()) // 81
let getXx = module1.getX;
console.log(getXx()) // undefined 这种方式获取的仅仅是个函数指针(函数地址), 此时的函数没有和module1对象没有绑定
// 下面这个函数才被绑定到 module1 对象上, 此时函数被调用才能够显示为 81
let xx = getXx.bind(module1);
console.log(xx()) // 81
注意
setTimeout函数的情况
setTimeout函数里面的 this 指向的是 window, 但是我们有些时候需要使用的不是 window 对象, 所以需要我们主动的绑定当前函数或者我们需要的this 到参数中
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function () {
// 这里把 setTimeout 中的 this(指向的是 window 对象), 修改成函数 bloom 的 this 对象
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function () {
// 如果函数调用中的 this 需要指向的是 LateBloomer 而不是 setTimeOut 导致的 window 对象
console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};
const flower = new LateBloomer();
flower.bloom(); // 一秒钟后, 调用 'declare' 方法
看不懂的, 需要配合里面的注释
当然我们可以直接传递函数, 而不是bind函数
window.setTimeout(this.declare, 1000);
这样我们就能够看见下面的 window 对象输出了
改回到 window.setTimeout(this.declare.bind(this), 1000); 我们又能看到
绑定构造函数的参数
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + ',' + this.y;
};
const p = new Point(1, 2);
console.log(p.toString()); // '1,2'
const emptyObj = {};
const YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// var YAxisPoint = Point.bind(null, 0/*x*/);
const axisPoint = new YAxisPoint(5);
console.log(axisPoint.toString()); // '0,5' 第一个参数被绑定为 0 了
console.log(axisPoint instanceof Point); // true
console.log(axisPoint instanceof YAxisPoint); // true
console.log(new YAxisPoint(17, 42) instanceof Point); // true
JavaScript的函数参数是数组
JavaScript函数的参数使用的是一个数组, 而不是Array, 所以我们可以使用arguments[0]
JavaScript在调用函数时, 传递的参数可以是无, 也可以是无限(前提是不超过数组的最大长度, 并且内存不溢出)
注意: 箭头函数没有一般函数的
arguments参数
箭头函数中的arguments参数
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问
function foo() {
console.log(arguments[0]);
}
foo(5);
let bar = () => {
// 下面参数不能够得到我们需要的结果
console.log(arguments[0]);
console.log(arguments);
}
bar(5);
console.log(arguments); 这段函数打印:
[Arguments] {
'0': {},
'1': [Function: require] {
resolve: [Function: resolve] { paths: [Function: paths] },
main: Module {
id: '.',
path: 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数',
exports: {},
filename: 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数\\func.js',
loaded: false,
children: [],
paths: [Array]
},
extensions: [Object: null prototype] {
'.js': [Function (anonymous)],
'.json': [Function (anonymous)],
'.node': [Function (anonymous)]
},
cache: [Object: null prototype] {
'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数\\func.js': [Module]
}
},
'2': Module {
id: '.',
path: 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数',
exports: {},
filename: 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数\\func.js',
loaded: false,
children: [],
paths: [
'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数\\node_modules',
'D:\\programs\\codes\\JavaScript\\javascript_demo\\node_modules',
'D:\\programs\\codes\\JavaScript\\node_modules',
'D:\\programs\\codes\\node_modules',
'D:\\programs\\node_modules',
'D:\\node_modules'
]
},
'3': 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数\\func.js',
'4': 'D:\\programs\\codes\\JavaScript\\javascript_demo\\05函数'
}
说明箭头函数还是有 arguments 数组的, 但和普通函数的 arguments 有所不同
我们可以这样用:
function foo1() {
let bar1 = () => {
console.log(arguments[0])
}
bar1()
}
foo1(3);
后面我们还可以用
收集参数来实现arguments的方式
ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用
没有函数重载
ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载
如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的
没有函数重载但可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载
把函数名当成指针也有助于理解为什么 ECMAScript 没有函数重载, 因为指针变量名相同了
指针是地址
默认参数值
function makeKing(name = 'Henry') {
return `King ${name} VIII`;
}
console.log(makeKing())
console.log(makeKing('100'))
arguments对象始终以调用函数时传入的值为准, 不在乎默认函数参数的值是什么
function makeKing(name = 'Henry') {
console.log(arguments[0]); // undefined
name = "haha";
console.log(arguments[0]); // undefined
return `King ${name} VIII`;
}
console.log(makeKing())
注意上面的两处
undefined
默认函数参数也可以使用调用函数返回的值
function getName() {
return "haha"
}
function makeKing(name = getName()) {
return `King ${name} VIII`;
}
console.log(makeKing()) // King haha VIII
- 函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值
- 计算默认值的函数只有在调用函数但未传相应参数时才会被调用
箭头函数也可以使用默认参数
默认参数作用域与暂时性死区
默认函数参数作用域
默认函数参数在使用上时, 就会像 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}`;
}
参数扩展与收集
扩展参数(把数组变量分成多个元素, 然后传递给函数的多个参数)
在传递一个数组参数的时候, 我们不希望它传递的是一个数组而是数组内的多个元素, 这时候以前的时候将会是这样:
let values = [1, 2, 3, 4];
function getSum() {
let sum = 0;
for (let argument of arguments[0]) {
sum += argument
}
return sum;
}
console.log(getSum(values))
我们还可以使用apply调用:
let values = [1, 2, 3, 4];
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
console.log(getSum.apply(null, values))
而ES6引入了新的方式
console.log(getSum(...values));
我们还可以配合默认参数
let params = [1, 'haha', null];
function getSum(param1, param2, param3, name = "haha") {
console.log(typeof param1);
console.log(typeof param2);
console.log(typeof param3);
console.log(name)
}
/*
number
string
object
haha
*/
displayType(...params)
但这样不方便, 数组可能有好多元素, 不可能函数参数也整十来个??? 解决方案是
收集参数
收集参数(其他语言中的可变参数)
let params = [1, 'haha', null];
function displayType(...values) {
for (let argument of arguments) {
console.log(typeof argument);
}
}
/*
number
string
object
*/
displayType(...params)
使用收集参数需要注意
收集参数和函数参数位置需要注意
function displayType(name, ...values)
function displayType(name = "haha", ...values)
这样是可以的
function displayType(...values, name)
这样是不行的
箭头函数可以有收集参数
let getSum = (...values) => {
for (let value of values) {
console.log(value)
}
}
getSum(1,2,3);
前面我们知道, 箭头函数不支持 arguments 参数(不是不支持, 是和function函数的不同)
我们使用 收集参数试试
let getSum02 = (...arguments) => {
for (let argument of arguments) {
console.log(argument)
}
}
getSum02(1, 2, 3); // 1 2 3
发现是没问题的
前面说的箭头函数不支持
arguments, 在这里得到解决
函数声明与函数表达式
函数的声明和函数表达式大体上是一样的, 但是在实际JavaScript引擎加载时是有区别的
函数声明(会提升到作用域顶部)
print()
function print() {
console.log("hello world")
}
js引擎在加载代码前, 会扫描一遍代码, 把函数声明加载到源代码树的顶部, 并且在执行上下文的顶部, 添加函数定义
换句话说就是把函数声明提升到该作用域中最前面, 即使我们将该函数写在作用域的最后面, 都会被提升
相同的情况我们换成函数表达式就不行了
函数表达式
print();
let p = function () {
console.log("hello world");
}
即便你把 let 改成 var 也是
print();
var p = function () {
console.log("hello world");
}
因为你把函数的定义留在了变量中, 而变量的赋值需要代码执行到那一行, 但上面的print函数执行时, 下面的变量var p虽然声明提升到作用域顶部, 但它还没被赋值值为undefined, 等到执行var p = function 时函数定义才会被存储
console.log(a) // undefined
var a = 10
console.log(a) // 10
所以只有函数声明会提升, 函数表达式的形式没有提升
函数作为值(函数作为另一个函数的参数或返回值)
因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
function add10(num) {
return num + 10;
}
// 把 add10 当作 callSomeFunction 的参数传递进去
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name) {
return "Hello, " + name;
}
// 把 getGreeting 当作 callSomeFunction 的参数传递进去
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas"
函数返回值是另一个函数
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;
}
};
}
let comparisonFunction = createComparisonFunction(2);
let arr1 = [1, 2, 3];
let arr2 = [3, 4, 5];
console.log(comparisonFunction(arr1, arr2)) // -1
函数内部
函数内部有arguments和this, 在ES6之后又有了new.target属性
arguments
arguments 还有个属性叫callee, 指向的是该函数
function factorial(num) {
if (num <= 1) {
return 1;
} else {
// return num * factorial(num - 1);
return num * arguments.callee(num - 1);
}
}
console.log(factorial(5));
arguments代替了原先的硬编码函数名
this
它在标准函数和箭头函数中有不同的行为
- 在标准函数中,
this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)。 - 在箭头函数中,
this引用的是定义箭头函数的上下文。
一个是调用的上下文, 一个是定义的上下文, 区别在这里
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
箭头函数:
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
箭头函数的
this和普通函数的this指向的对象可能不相同, 箭头函数的this有穿透一层作用域的效果(穿透一个花括号的效果, 该效果仅在定义时使用, 有点像静态this, 在运行时this就不再更改), 如果定义时外层作用域中没有this则箭头函数也没有this, 如果有this, 那么箭头函数的this就是它, 而普通函数的this没有, 普通函数只在乎谁调用了它, 它的this就是谁, 这和kotlin一样
如果不理解的, 可以参考 c++ 的 lambda 表达式如何捕获的外部 this 对象,
[this]() {}, 这样写就捕获了外部作用域的this, 当然如果想捕获外部的变量则是[a]() {}, 这样就捕获了外部作用域的a变量, 我们的箭头函数也是, 他会捕获外部作用域的this指针
箭头函数的this的使用场景
有些时候, 函数的 this 并非我们想要的 this
function A() {
this.name = "11111";
setTimeout(function () {
console.log(this.name)
}, 1000)
}
function B() {
this.name = "22222";
setTimeout(() => {
console.log(this.name)
}, 1000)
}
new A() // undefined
new B() // 22222
function A() {
this.name = "11111";
return function () {
console.log(this.name)
}
}
function B() {
this.name = "22222";
return () => {
console.log(this.name)
}
}
new A()() // undefined
new B()() // 22222
函数名只是保存指针的变量。因此全局定义的 sayColor()函数和 o.sayColor()是同一个函数,只不过执行的上下文不同。
caller
caller是调用该函数的函数, 如果在全局作用域中调用, 则为 null
function outer() {
inner()
}
function inner() {
console.log(inner.caller)
}
outer() // [Function: outer]
function outerMethod() {
innerMethod()
}
function innerMethod() {
console.log(arguments.callee); // innerMethod
console.log(innerMethod.caller); // outerMethod
console.log(arguments.callee.caller); // outerMethod
}
outerMethod()
严格模式下, 这些都是错的, 在非严格模式下可用
new.target
ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用
ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数
说白了就是判断是否使用
new 函数()这种方式调用的, 如果是的话new.target则不为undefined
function King() {
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
这也是我们前面实现抽象类的方式
函数属性与方法
函数也是有两个属性的, length 和 prototype
length显示的是方法参数的数量, 而prototype是原型对象
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:
prototype属性上的方法能够在多个对象中共享, 所以多数对象都有toString, valueOf等方法
函数还有两个函数apply和call
这两个方法都会以指定的 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
记住
arguments只有调用时传入的参数才算
在严格模式下,调用函数时如果没有指定上下文对象,则
this值不会指向window。除非使用apply()或call()把函数指定给一个对象,否则this的值会变成undefined
call() 方法与 apply() 的作用一样,只是传参的形式不同。通过 call() 向函数传参时,必须将参数一个一个地列出来
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}
console.log(callSum(10, 10)); // 20
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
看到这里的
call和apply的特性this, 有没有想到前面的bind函数的第一个参数也是this
bind() 方法会创建一个新的函数实例,其 this 值会被绑定到传给 bind()的对象
window.color = 'red';
var o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
函数表达式
我们知道,定义函数有两种方式:函数声明和函数表达式
前面说过的 函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。而函数表达式是执行到变量时才会被定义
// 右边的形式是匿名函数
const functionName = function (arg0, arg1, arg2) {
// 函数体
}
// 右边的是lambda表达式函数(也是匿名函数)
const lambdaFunction = (arg0, arg1, arg2) => {
// 函数体
}
有的时候函数声明会有坑, 这坑要了解函数声明的提升, 比如下面的代码就会出问题:
if (condition) {
function sayName() {
console.log(1)
}
}
else {
function sayName() {
console.log(2)
}
}
sayName()
在实际执行的过程中, 大多数浏览器都不能够根据condition变量的值判断到底执行第一个sayName还是第二个sayName, 最终打印的是 1 还是 2 不同的浏览器可能返回不同的值
原因非常简单, 还是函数声明的上下文作用域提升问题
由于两个函数的函数名相同, 提升到源码树上却只有一个节点, 这样的话, sayName打印1的函数通常会被替换成sayName打印2的函数
所以通常都会返回第二个函数(但也不确定)
即使没问题也别这么用
那么要怎么解决呢?
很简单, 把函数声明替换成函数表达式就行
let condition = true;
let sayName
if (condition) {
sayName = function () {
console.log("1")
}
} else {
sayName = function () {
console.log("2")
}
}
sayName()
命名函数表达式
const factorial = (function f(num) {
if (num <= 1) {
return num;
} else {
return num * f(num - 1)
}
});
console.log(factorial(3)) // 6
前面我们说的严格模式下
arguments.callee会报错, 代替方案就是命名函数表达式
尾调用优化
内存管理优化机制, 就是在一个函数return返回另一个函数
function outerFunction() {
return innerFunction(); // 尾调用
}
在es6之前, 会这样执行
- 执行到
outerFunction, 会入栈帧 - 执行到
return需要调用innerFunction - 执行
innerFunction, 再入栈帧 - 执行函数
innerFunction再返回 - 将
innerFunction的返回值传递给outerFunction, 然后再执行outerFunction的返回值 outerFunction将栈帧弹出栈外。
在ES6之后:
- 执行到
outerFunction, 第一个栈帧被推到栈上 - 执行到
return需要调用innerFunction - 弹出
outerFunction的栈帧(因为最终的返回值在innerFunction, 所以outerFunction的栈帧已经不重要了) - 执行
innerFunction, 入栈帧 - 执行
innerFunction函数体,计算其返回值 - 将
innerFunction的栈帧弹出栈外
这样栈帧提前被弹出了
现在还没有办法测试尾调用优化是否起作用。不过,因为这是 ES6 规范所规定的,兼容的浏览器实现都能保证在代码满足条件的情况下应用这个优化。
尾调用优化的条件
尾调用优化的条件就是确定外部栈帧真的没有必要存在了, 涉及的条件有:
- 代码在严格模式下执行
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
举几个不符合尾调用优化的情况:
"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();
}
这个优化在递归场景下的效果是最明显的,因为递归代码最容易在栈内存中迅速产生大量栈帧
之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用
f.arguments和f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效,以防止引用这些属性。
尾调用优化的代码
明显下面代码不支持尾调用优化:
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
console.log(fib(0)); // 0
console.log(fib(1)); // 1
console.log(fib(2)); // 1
console.log(fib(3)); // 2
console.log(fib(4)); // 3
console.log(fib(5)); // 5
console.log(fib(6)); // 8
因为上面代码返回的函数多了个+号
我们需要改造下
"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);
}