ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JS中函数(二)(arguments、this、call、apply、bind、TCP)

2021-10-27 17:00:51  阅读:195  来源: 互联网

标签:function 调用 console log bind TCP JS arguments 函数


JS中函数(二)(arguments、this、call、apply、bind、TCP)

  这是函数章节第二部分总结内容。设计到函数内部的 arguments、this、caller、new.target 等属性和 apply、call、bind 方法。以及递归、ES6 后的尾调用优化TCP的相关内容。
其中比较重要的 apply、call、bind 方法的原理请参考 手撕JavaScript call apply bind 函数

10.9、函数内部

  在 ECMAScript5 中,函数调用时内部存在两个特殊的对象:arguments this。ECMAScript6 又新增了 new.target 属性。

10.9.1 arguments

  arguments 对象讨论过多次,它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(除了使用箭头语法创建函数)时才会有。

function a (){		// 函数声明方式
    console.log(arguments.length);
}			
a(1,2,3);		// 3
let b = function(){		// 函数表达式方式
    console.log(arguments.length);
};
b(4,5,6);		// 3	
let c = new Function("console.log(arguments.length)");	// 使用 Function 构造函数
c(1,2,3,4);		// 4	
let d = () => console.log(arguments.length);	// 箭头函数
d(1,2,3);		// 报错 ReferenceError: Can't find variable: arguments

   arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。(类似于构造函数的原型对象中的 construction 属性。)来看下面这个经典的阶乘函数:

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

  阶乘函数一般定义成递归调用的,这个函数要正确执行就必须保证函数名是 factorial 因为内部代码递归写的是 factorial,从而导致了紧密耦合。 使用 arguments.callee 就可以让函数逻辑与函数名解耦

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

  这个重写之后的 factorial() 函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。

let trueFactorial = factorial;
 factorial = function() {
   return 0;
};
console.log(trueFactorial(5));  // 120
console.log(factorial(5));      // 0

10.9.2 this

  函数中另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。

  • 在标准函数中

  在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。来看下面的例子:

window.color = 'red';
let o = {
  color: 'blue'
};
function sayColor() {
  console.log(this.color);
}
sayColor();    	// 'red'
o.sayColor = sayColor;// 给对象创建一个方法sayColor指向全局中的sayColor
o.sayColor();  	// 'blue'

  定义在全局上下文中的函数 sayColor() 引用了 this 对象。这个 this 到底引用哪个对象必须到函数被调用时才能确定。这个值在代码执行的过程中可能会变。在全局上下文中调用 sayColor() ,调用对象其实是 window。所以打印 this.color 相当于 window.color。o.sayColor(),this 会指向 o 对象, 即 this.color 相当于 o.color,所以会显示"blue"。

  • 在箭头函数中

  在箭头函数中,this 引用的是定义箭头函数的上下文对象

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

  上述的例子中,箭头函数在全局上下文中定义,即在 window 上下文中定义的。所以在对 sayColor() 的两次调用中,this 引用的都是 window 对象。

  很多时候在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。就是因为箭头函数中的 this 会保留定义该函数时的上下文:

function King() {
	this.royaltyName = 'Henry';
	// this 引用 King 的实例
	setTimeout(() => console.log(this.royaltyName), 1000);
}
function Queen() {
  	this.royaltyName = 'Elizabeth';
	// this 引用 window 对象
  	setTimeout(function() { console.log(this.royaltyName); }, 1000);
}
new King();  // Henry
new Queen(); // undefined

  看了这段代码,可能也很难明白,这里的定时回调函数或者事件回调函数都不会直接进入执行栈中执行的,而是会先放在任务队列中等到执行栈中代码都执行完了再满足情况下再进入执行栈中执行,也就是说 King() 和 Queen()函数中的定时器其实都是等到函数执行完了之后再进入执行栈中执行的,相当于在全局上下文环境中,所以回调函数中的普通函数内的 this 指向当前环境的调用者 window,而 window 对象没有 royaltyName 属性,于是打印 undefined。而箭头函数在定义时就确定了自己的 this 指向定义时所处的上下文环境中是 King() 构造函数上下文,所以最后执行的时候,箭头函数内的 this 指向King() 的实例,打印的是实例内的属性值 Henry。

箭头函数和普通函数的区别

10.9.3 caller

  ECMAScript5 也会给函数对象上添加一个属性:caller。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。比如:

unction outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
} 
outer();		// 返回 outer() 函数 因为 ourter()调用了 inner()

  如果要降低耦合度,则可以通过 arguments.callee.caller来引用同样的值:

function outer() {
  inner();
}
function inner() {
  console.log(arguments.callee.caller);
} 
outer();

  注意,箭头函数是没有 caller(调用当前函数的函数)、arguments(调用当前函数传入的参数构成的数组)、callee 属性的。

  在严格模式下访问 arguments.callee 会报错。ECMAScript 5也定义了 arguments.caller,但在严格模式下访问它会报错,在非严格模式下则始终是 undefined。严格模式下还有一个限制,就是不能给函数的 caller 属性赋值,否则会导致错误。

10.9.4 new.target

  ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。(箭头函数不可用作构造函数,可以当做普通函数)

  new.target 属性允许你检测函数或构造方法是否是通过 new 运算符被调用的。如果函数是正常调用的,则 new.target 的值是 undefined。如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数

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"

10.10、函数属性与方法

  ECMAScript 中的函数是对象,因此有属性和方法。

  • 函数对象的属性

  每个函数都有三个属性:length 、name 和 prototype。注意:箭头函数只有两个:length 和 name。

// 获取 Function 构造函数拥有的属性
console.log(Object.getOwnPropertyNames(Function));//["length", "name", "prototype"]
console.log(Object.getOwnPropertyNames(()=>{}));  // ["length", "name"]

  length 属性保存函数定义的命名参数的个数。

  name 属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。前面10.2中有讲到。

  prototype 属性保存该对象的原型对象地址。

function a (){};
let b = function(num1){return num1;};
let c = (num1,num2)=>num1+num2;
let d = new Function("num1","num2","return 0");	
// 分别获取函数对象拥有的属性
console.log(Object.getOwnPropertyNames(a)); //["length", "name", "prototype"]
console.log(Object.getOwnPropertyNames(b)); //["length", "name", "prototype"]
console.log(Object.getOwnPropertyNames(c)); //["length", "name"]
console.log(Object.getOwnPropertyNames(d)); //["length", "name", "prototype"]
console.log(a.length);  // 0
console.log(b.length);  // 1
console.log(c.length);  // 2
console.log(d.length);  // 2
console.log(a.name);    // a
console.log(b.name);    // b
console.log(c.name);    // c
console.log(d.name);    // anonymous
console.log(a.prototype);   // a构造函数的原型对象
console.log(b.prototype);   // b构造函数的原型对象
console.log(c.prototype);   // undefined 相当于创建了一个 prototype 属性没有复制
console.log(d.prototype);   // anonymous构造函数的原型对象

  可以通过获取属性的描述来进一步了解属性:

console.log(Object.getOwnPropertyDescriptor(a,"length"));
console.log(Object.getOwnPropertyDescriptor(b,"name"));
console.log(Object.getOwnPropertyDescriptor(d,"prototype"));
函数属性描述

  由上图可知,函数对象内的属性都是不可迭代的,所以不能用 for-in 或者 Object.keys() 遍历这些属性。

  当我们用函数去创建一个实例对象时,实例对象会有一个隐含的 [[prototype]] 属性指向创建该实例对象的构造函数的原型对象。所以实例对象可以调用原型对象中的方法。

let b = function(num1){return num1;};
let bObj = new b();

  上述代码中函数对象、函数实例对象、函数的原型对象三者的关系图:

函数、函数对象、函数的原型对象关系图

  prototype 属性是 ECMAScript 核心中最重要的部分。prototype 是保存引用类型所有实例方法的地方,这意味着toString()、valueOf()等方法实际上都保存在相应对象的 prototype 上,进而由所有实例共享。这个属性在自定义类型时特别重要。

  • 函数对象的方法

  这里主要介绍函数的原型对象中的三个方法:apply()call()bind() 方法,比较相似。三个方法都是用来给函数指定函数体内的 this 值的。他们的调用者都是函数对象。

  apply() 和 call() 两个方法除了给调用者传递 this 值,还会立即执行函数。返回函数执行的结果。他们的第一个参数就是要传递的 this 值,第一个参数不指定 this 值即空着或者填 null 都将指向 window。后面的参数会传递给调用的函数。call() 后面传递给函数的参数是是 逐个传递的。,apply() 后面传递给函数的参数需要放入一个数组中传递。

  bind() 方法会创建一个新的函数,其 this 值会被绑定到传给 bind() 的对象,返回新的函数。可以在绑定的时候传入调用函数需要的参数,也可以在调用绑定后的函数时传递参数,

var course = "JS";		// var 声明的变量会作为 window 对象的属性
let price = 123;		// let 声明的变量保存在 全局 scope 中
function fun (){
  let arr = Array.prototype.slice.call(arguments,0); // 将传入参数转为数组
  let sum = arr.reduce((a,b)=>a+b,0);		// 计算传入参数的和
  console.log(this);
  console.log(this.course);
  console.log(this.price);
}
o = {		// 自定义的对象
  course:"css",
  price:980
}
fun.call(null,1,2,3,4);	// 10 // window // JS // undefined 
fun.apply(o,[1,2,3,4]);	// 10 // o // css // 980 
let f = fun.bind(o);
f();	// 0 // o // css // 980 

  上述代码中,我们利用了 call() 将 arguments 类数组对象转化为数组对象。然后利用数组的 reduce() 将参数求和。

10.11、函数表达式

  定义函数有四种方式:函数声明、函数表达式、箭头函数、Function 构造函数。

  前面讲到过函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后:

sayHi();
function sayHi() {
  console.log("Hi!");
}

  而函数表达式不会提升,函数表达式有几种不同的形式,最常见的是这样的:

let functionName = function(arg0, arg1, arg2) { 
    // 函数体
};

  函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数 (anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数也称为兰姆达函数)。未赋值给其他变量的匿名函数的 name 属性是空字符串。

// 千万别这样做! 
if (condition) {
  function sayHi() {
    console.log('Hi!');
  }
} else {
  function sayHi() {
    console.log('Yo!');
  } 
}

  多数浏览器会忽略 condition 直接返回第二个声明,因为他们都是函数声明方式,都会提升第二个会覆盖第一个。如果换成函数表达式声明,就会达到预期的效果。

// 没问题
let sayHi;
if (condition) {
  sayHi = function() {
    console.log("Hi!");
};
} else {
  sayHi = function() {
    console.log("Yo!");
}; }

  创建函数并赋值给变量的能力也可以用于在一个函数中把另一个函数当作值返回,例如前面提到的根据对象属性排序的算法。

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

  任何时候,只要函数被当作值来使用,它就是一个函数表达式。

10.12、递归

  递归函数通常的形式是一个函数通过名称调用自己:

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}

  如果把这个函数赋值给其他变量 anotherFactorial,然后将 factorial 设置为null,于是只保留了一个对原始函数的引用。调用 anotherFactorial() 时,要递归调用 factorial(),但因为它已经不是函数了,所以会出错。

et anotherFactorial = factorial; 
factorial = null; console.log(anotherFactorial(4)); // 报错

  前面讲过可以用 arguments.callee 解决。arguments.callee就是一个指向正在执行的函数的指针。

unction factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}

  因此在编写递归函数时,arguments.callee 是引用当前函数的首选。

不过,在严格模式下运行的代码是不能访问arguments.callee的,访问会出错。此时,可以使用命名函数表达式达到目的。

const factorial = (function f(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
});

  这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。

10.13、尾调用优化 TCP (Tail Call Optimization)

  先来明白什么是尾调用:即外部函数的返回值是一个内部函数的返回值。比如:

function outerFunction() {
	return innerFunction(); // 尾调用
}

  ECMAScript6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。这项优化非常适合“尾调用”。

  在ES6 优化之前,执行这个例子会在内存中发生如下操作:

(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。

(2) 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。

(3) 执行到 innerFunction 函数体,第二个栈帧被推到栈上。

(4) 执行 innerFunction 函数体,计算其返回值。

(5) 将返回值传回 outerFunction ,然后 outerFunction 再返回值。

(6) 将栈帧弹出栈外。

  在ES6优化之后,执行这个例子会在内存中发生如下操作。

(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。

(2) 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。

(3) 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返 回值也是outerFunction 的返回值。

(4) 弹出 outerFunction 的栈帧。

(5) 执行到 innerFunction 函数体,栈帧被推到栈上。

(6) 执行innerFunction函数体,计算其返回值。

(7) 将innerFunction的栈帧弹出栈外。

  很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。 而第二种情况下无论调用多少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。

10.13.1 尾调用优化的条件

  尾调用优化的条件就是确定外部栈帧真的没有必要存在了。涉及的条件如下:

  • 代码在严格模式下执行;
  • 外部函数的返回值是对尾调用函数的调用;
  • 尾调用函数返回后不需要执行额外的逻辑;
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包。

  下面列举违反上述条件的调用情况:

"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();
}

  差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用,都可以应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。在递归过程中优化效果比较明显,因为递归代码最容易在栈内存中迅速产生大量栈帧。

  之所以要求严格模式,因为在非严格模式下函数调用中允许使用 f.arguments 和 f.caller,而它们都会引用外部函数的栈帧。显然,这意味着不能应用优化了。因此尾调用优化要求必须在 严格模式下有效,以防止引用这些属性。

10.13.2 尾调用优化的代码

  通过把简单的递归函数转换为待优化的代码来加深对尾调用优化的理解。下面是一个通过递归计算斐波纳契数列的函数:

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

  显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作。结果,fib(n) 的栈帧数的内存复杂度是O(2n) 。

  把递归改写成迭代循环形式改进。不过,也可以保持递归实现,但将其重构为满足优化条件的形式。为此可以使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

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

  这样重构之后,就可以满足尾调用优化的所有条件,再调用 fib(1000) 就不会对浏览器造成威胁了。

标签:function,调用,console,log,bind,TCP,JS,arguments,函数
来源: https://blog.csdn.net/ItDaChuang/article/details/120996593

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有