四、《JavaScript高级程序设计》笔记

131 阅读13分钟

代理

代理的运用场景

跟踪属性访问

根据对基本操作 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; }

箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 argumentssupernew.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 对象输出了

image.png

改回到 window.setTimeout(this.declare.bind(this), 1000); 我们又能看到

image.png

绑定构造函数的参数
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

函数内部

函数内部有argumentsthis, 在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指针

image.png

箭头函数的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"

这也是我们前面实现抽象类的方式

函数属性与方法

函数也是有两个属性的, lengthprototype

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等方法

函数还有两个函数applycall

这两个方法都会以指定的 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

看到这里的 callapply 的特性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之前, 会这样执行

  1. 执行到outerFunction, 会入栈帧
  2. 执行到return需要调用innerFunction
  3. 执行innerFunction, 再入栈帧
  4. 执行函数innerFunction再返回
  5. innerFunction的返回值传递给outerFunction, 然后再执行outerFunction的返回值
  6. outerFunction将栈帧弹出栈外。

在ES6之后:

  1. 执行到outerFunction, 第一个栈帧被推到栈上
  2. 执行到return需要调用innerFunction
  3. 弹出outerFunction的栈帧(因为最终的返回值在innerFunction, 所以outerFunction的栈帧已经不重要了)
  4. 执行innerFunction, 入栈帧
  5. 执行 innerFunction 函数体,计算其返回值
  6. 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.argumentsf.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);
}