js基础升级打怪(三 函数 === 方法 ?函数声明 === 函数表达式 ? 执行上下文 )

97 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

image.png

函数 === 方法 ?

函数(function)

概念

A function is a code snippet that can be called by other code or by itself, or a variable that refers to the function. When a function is called,arguments are passed to the function as input, and the function can optionally return a value. A function in JavaScriptis also an object.

函数是一个代码片段,可以被自己、其他代码或者 指向该方法的变量调用。函数调用时arguments会被当作入参,出参可有可无。函数也是一个对象类型。

To use a function, you must define it somewhere in the scope from which you wish to call it.

简而言之,函数就是代码片段,就是{}里面的东西,为了方便被其他函数(另一块代码片段)调用,得给他起一个名字,就是函数定义/声明。

方法(method)

A method is a function which is a property of an object.
方法是一个对象的属性。

换言之:

When F is said to be a method of O, it often means that F uses O as its this binding.

函数包含方法

  • 没有绑定this的是一个独立函数
  • 绑定this的是对象函数 《---》 方法
  • js代码里的所有函数,都可以称之为方法,node环境下this是内置的global,浏览器环境下this是内置的window或者globalThis;
  • 概念上,函数包含方法;
  • 实际使用上,函数 === 方法;

下文中函数就是方法,方法就是函数,不做严格区分;

函数声明 === 函数表达式 ?

At a high level, an expression is a valid unit of code that resolves to a value.
解析值的有效代码单元。或者说是计算功能的最小单位。

  1. js代码 等价于 一组语句;
    JavaScript applications consist of statements;

  2. 一条语句 等价于 一组关键字 等价于 表达式/声明的组合;
    A single statement isn't a keyword, but a group of keywords;

  3. 表达式 包含

  • 基本表达式
  • 基本表达式和操作符(+、-、&&、 || 等)组成的复杂表达式
  1. 基本表达式
  • super this

  • new

  • function

  • ()括号操作符

  • []

  • {}

  1. 关键字 包含
  • 声明变量 var、 let、var
  • 方法和类 function、 async function、class等
  • 控制流关键字 return、 if、 break等
  • 循环关键字 for、 while、 do...while、 for...in、 for...of等
  • 其他 debugger、 import、export等

所以 函数表达式和函数声明是一个并列的关系,一起定义了一个函数;
比如: function test(){}
通过这种方式创建一个函数
实际上,计算机的操作是,var test = function test(){}, 创建test方法色同时,也将方法赋值给一个同名变量; 这样我们才能在调用test()时候能访问到test。

  • 只有表达式 1 + 1;计算得出2, 计算后的值没有地方保存,删除2;这就是计算机的操作;
  • var a = 1 + 1; 计算后保存到声明的变量a中;
  • 表达式 ===》 计算
  • 声明 ===》 存值

一、函数

  • 函数定义 等价于 函数声明 等价于 函数语句
  • 不等同于 函数表达式

  • 函数表达式也是函数字面量,字面量就是所见即所得,可以看作一个常量,像数字、字符、false、true等

函数创建有这两种方式

// 函数声明 Function declarations
function square(number) {
  return number * number;
}

// 函数表达式 匿名函数Function expressions
const square = function (number) {
  return number * number;
}

// 函数表达式 具名函数Function expressions
const square = function funcName (number) {
  return number * number;
}

函数包含三部分

  • 名字
  • 参数(这里指入参,包含在口号内(),逗号分隔)
  • 方法体(包含在花括号内{})
  • 方法 = 方法名 + 圆括号() + 花括号{}

探讨几个问题

以函数名、 ()、{}为 出发点

1. 函数调用有几种方式

三种方式:

  • 自己调用自己,也就是递归函数;
  • 被其他函数调用,或者作为独立函数执行;
  • 被函数的引用变量调用(表达式定义调用);

But:

  • 方法名只能在函数体内调用,不能在方法外调用;

  • 方法的调用都是通过方法引用变量;

  • 函数声明创建的方法之所以能使用方法名,是因为函数声明的同时,创建了一个和方法名同名的变量,所以函数声明创建的方法,在函数体内外都能通过这名字访问该方法;

  • 方法名不能改变;方法引用变量可以再赋给其他变量;

const y = function x() {};
console.log(x); // ReferenceError: x is not defined

2. 行参 和 实参是一一对应的吗? 返回值是必须有的吗?

参数
  • 下例子中,行参是()中列出的a,b
  • 函数调用test(1), 实参是1,也是方法体中的arguments
  • 行参初始化,a = undefined, b = undefined;
  • 实参个数 < 行参个数, 多余的行参数,仍是undefined;
  • 实参个数 > 行参个数, 多余的实参,被忽略,实参数只能借arguments取到;
  • 在没有剩余参数的情况下,a, arguments之间,相互关联,更改a的值影响arguments[0],更改arguments[0]影响a的值;因为b在调用时没有赋值, arguments[1]和b互不影响;
  • 严格模式下,获取不到arguments, caller, callee
function test( a, b ){

  console.log(a, b)
  console.log(arguments)
  
  // 1 undefined
  // [Arguments] { '0': 1 }
  
  a = '1a'
  b = '1b'
  console.log(a, b)
  console.log(arguments)
  
  // 1a 1b
  // [Arguments] { '0': '1a' }
  
  arguments[0] = '2a'
  arguments[1] = '2b'
  console.log(a, b)
  console.log(arguments) 
  
  // 2a 1b
  // [Arguments] { '0': '2a', '1': '2b' }
}

test(1)  


// 严格模式,在代码头部添加
 "use strict"

行参中包含剩余参数
function test( a, ...restAargs ){

  console.log(a, restAargs)
  console.log(arguments)
  
  // 1 [ 2 ]
  // [Arguments] { '0': 1, '1': 2 }

  a = '1a'
  console.log(a, restAargs)
  console.log(arguments)
  
  // 1a [ 2 ]
  // [Arguments] { '0': 1, '1': 2 }
  arguments[0] = '2a'
  console.log(a, restAargs)
  console.log(arguments) 
  
  // 1a [ 2 ]
  // [Arguments] { '0': '2a', '1': 2 }

  restAargs[0] = '3a'
  console.log(a, restAargs)
  console.log(arguments) 
  // 1a [ '3a' ]
  // [Arguments] { '0': '2a', '1': 2 }
}

test(1, 2) 
返回值

普通方法返回值

  • 方法体中没有return语句默认返回,undefined;
  • 有return语句,但是没有返回数据,返回值还是undefined;
  • 否则,给什么返回什么;

new关键字创建的方法

  • 没有return, 默认返回test原型;

  • return 基本数据类型,数据被忽略, 仍然返回方法原型;

  • return 引用数据, 给什么返回什么;

function test(){
  return;
  // undefined
  // test {}
  
  return 0;
  // 0
  // test {}
  
  return {};  
  // {}
  // {}
}

console.log(test());
console.log(new test());

3. 修改参数,实参会被改变吗

如果参数是基本数据类型,实参数不会改变;
引用数据类型,array、object,实参数会改变;

function test(arr){
  arr[0] = 'new'
}
let arr = ['old']
test(arr)
console.log(arr) // [ 'new' ]


function test(obj){
  obj.name = 'new'
}
let obj = {
  name: 'old'
}
test(obj)
console.log(obj)  // { name: 'new' }

4. 参数默认值

  • 只有当没有实参 或 实参是undefined时,才会使用默认值
function test(a='aaa', b='bbb'){
  console.log(a, b);
}

test(1, 2)  // 1 2
test()  // aaa bbb
test(undefined, undefined) // aaa bbb
  • 参数默认值的作用域和函数体的作用域不同;
  • a的作用域是方法体的外层作用域, 或者说是父级作用域;
  • 方法体内部的go(),函数声明提升,只提升到方法体最上层,不会到父级作用域;
  • 所以参数访问不到go()
  • 解决办法: 就是在父级作用域定一个方法go(),或者 值为go()的方法引用变量;
function f(a = go()) {
  function go() {
    return ":P";
  }
}

f(); // ReferenceError: go is not defined

// 解决
go = function(){ return ":P"; }
// 或
function go(){
  return ":P"; 
}

5. 剩余参数(restArgs) 和 arguments

restArgs必须放在参数的最后一位,且包含前缀...

区别:

  • restArgs是一个数组,并且自带初始值,就是空数组;arguments是一个类数组,不是真正的数组,数组所有的方法都不能使用,要想使用,必须先转换成数组;
  • arguments虽然不是数组,并不妨碍它有length属性;参数重只有剩余参数时,arguments.length == restArgs.length
  • 命名参数、restArgs 和 argument相互独立互不影响
function test(x, y, ...restArgs){
  console.log(restArgs instanceof Array, arguments instanceof Array, x, y, restArgs);
}
test(1)           // true false 1 undefined []
test(1, 2)        // true false 1 2 []
test(1, 2, 3)     // true false 1 2 [ 3 ]
test(1, 2, 3, 4)  // true false 1 2 [ 3, 4 ]

// arguments 转数组
const normalArray = Array.prototype.slice.call(arguments);
// 或
const normalArray2 = [].slice.call(arguments);
// 或
const normalArray3 = Array.from(arguments);

// 命名参数、restArgs 和 argument相互独立互不影响
function test(x, y, ...restArgs){
  restArgs[0] = '1'
  arguments[1] = '2'
  console.log(x, y, arguments, restArgs);
  x = '2'
  y = undefined
  console.log(x, y, arguments, restArgs); 
  restArgs.pop()
  console.log(x, y, arguments, restArgs); 
}
test(1, 2, 3, 4)
// log
 1 2 [Arguments] { '0': 1, '1': '2', '2': 3, '3': 4 } [ '1', 4 ]
 2 undefined [Arguments] { '0': 1, '1': '2', '2': 3, '3': 4 } [ '1', 4 ]
 2 undefined [Arguments] { '0': 1, '1': '2', '2': 3, '3': 4 } [ '1' ]

参数获取:

  • 函数定义时,参数放在括号里,逗号分隔
  • 如果括号中没有参数,可以通过arguments获取到;arguments是一个对象
function test(a='aaa', b='bbb'){
  console.log(arguments, typeof arguments);
}

test(1, 2)
test()
test(undefined, undefined)

[Arguments] { '0': 1, '1': 2 } object
[Arguments] {} object
[Arguments] { '0': undefined, '1': undefined } object

6. 函数是一个对象,又有哪些属性

  • length、name、 arguments、 caller、 prototype
function test(a='aaa', b='bbb'){
  console.log(Object.getOwnPropertyNames(test))
  console.log(test.name)
  console.log(test.length)  // 行参的个数
  console.log(test.arguments)
  console.log(test.caller)
  console.log(test.prototype)

}
 function other(){
  test(1, 2)
 }

 other()

[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
test
0
[Arguments] { '0': 1, '1': 2 }
[Function: other]
{}

// length补充
- length指的是方法的行参数,有默认值之后的参数,不参与到length中;如果第一个行参有默认值,则length为0;
- 剩余参数不参与length;
- 实参的个数不影响length, length只关乎行参;

二、函数声明提升

  • 函数声明提升
  • 函数表达式不提升
  1. 函数声明提升:可以在让函数在函数定义前被调用
  2. 通过函数声明定义的函数,等同于,定义函数 同时 将函数赋值给相同函数名字的变量,所以可以通过变量名调用。
// 表达式定义
reference() // Cannot access 'reference' before initialization
const reference = function test(){
}
reference()


//声明定义
test()
function test(){
}
test()

// 等价于
// function test(){} <==> var test = test(){}

三、 递归函数

概念

The act of a function calling itself, recursion is used to solve problems that contain smaller sub-problems. A recursive function can receive two inputs: a base case (ends recursion) or a recursive case (resumes recursion).

关键点

出口 和 递归体

练习

1. n的阶乘

function factorial(n){
  if(n === 1) return 1
  return n * factorial(n-1)
}
console.log(factorial(10)); // 3628800

2. 斐波那契数列

function fibonacci(n) {
  if(n <=2 ) return 1
  return fibonacci(n-2) + fibonacci(n-1)
}
console.log(fibonacci(10))

3. list 《=》 tree


const list = [
  {
    id: '0',
    name: 'root'
  },
  {
    id: '1',
    pid: '0',
    name: 'root-1'
  },
  {
    id: '2',
    pid: '0',
    name: 'root-2'
  },
  {
    id: '3',
    pid: '1',
    name: 'root-1-1'
  },
  {
    id: '4',
    pid: '2',
    name: 'root-2-1'
  },
  {
    id: '5',
    pid: '3',
    name: 'root-1-1-1'
  }
]
const root = ['0']
function formatListToTree(list, tree){
  if (!tree){
    tree = root.map((id) => {
      return list.filter(item => item.id === id)[0]
    })
  }
  tree.forEach(item => {
    item.children = list.filter(l => l.pid === item.id)
    if(!item.children.length) return
    item.children = formatListToTree(list, item.children)
  })
  return tree
}
console.dir(formatListToTree(list));

四、 立即执行函数、 匿名函数和函数表达式

IIFE (Immediately Invoked Function Expression)
又叫作 自执行匿名函数
Self-Executing Anonymous Function

// 1
function foo1(x){
  console.log(arguments);
  return x;
}
foo1(1,2,3,4,5);

// 2
function foo2(x){
  console.log(arguments);
  return x;
}(1,2,3,4,5);

// 3
(function foo3(x){
  console.log(arguments);
  return x;
})(1,2,3,4,5)

五、提升、全局this、执行上下文、作用域链

提升

问题? console.log(window.a)和console.log(a); 一样吗?
第一句: undefined;
第二句: 报错,ReferenceError: a is not defined;
这样呢? 这两句打印的就都是undefined;因为var让a提升到log语句的上面,执行之前看到var a就给a赋值undefined,再执行;

变量提升的时机,代码执行之前,也可说是预编译;

console.log(globalThis.a)  // node环境的用this,浏览器中用window,globalThis在两个环境中都能用
console.log(a);
var a;

// undefined
// undefined

提升的两种场景:
1、可以在声明之前使用变量的值;
2、可以在声明之前使用变量引用;
第一种指的是函数声明提升;
第二种指的是var变量提升;

console.log(test);
console.log(test());
console.log(a);

function test(){
  return 2
}
var a = 1;

//log
[Function: test]
2
undefined

执行上下文 Execution Context

执行上下文栈.png

以浏览器环境为例子:

预编译 --- GO

  • JS文件就是一个JS模块,js代码执行前会进行预编译,也可理解为生成一个GO对象;

  • GO对象包含浏览器内置的构造函数,Object、Array等、全局this、还有提升的变量和方法;

  • 变量提升的先后顺序是var变量、方法;前文也提到,函数声明实际上就是将函数赋值给一个与函数名同名的变量,放在全局才能调用到方法;如果定义了相同的方法名和变量,在变量赋值之前访问变量,取到的是方法;

    console.log(a); // [Function: a]
    function a(){}
    var a = 1
    console.log(a);  // 1
    
    image.png

代码执行 --- 全局执行上下文 --- GEC

  • 将预编译生成的GO对象入上下文栈;

  • 执行上下文是一个栈数据结构,符合栈的先进后出的特点;

  • js引擎事件处理机制是单线程,这里只看简单的例子们不考虑异步任务;

  • 全局任务队列入栈;

  • 任务执行;

  • 代码一行一行执行;

  • 遇到函数执行;

  • 函数执行的瞬间生成AO对象;

  • 函数getName的任务队列入栈;

  • geName函数执行;

  • ...遇到函数就入栈,函数执行完出栈,并且取消与上一季函数的关联;

  • getName函数执行完,getName销毁;

  • 回到全局上下文栈;

  • ... 继续往下执行;

    image.png

函数执行上下文 --- FEC --- AO对象 active object

  • 函数执行上下文栈,首先要保存上一级的作用域,就是GO,保存的是引用,不是复制;

  • 函数执行的瞬间生成AO对象;

  • AO对象属性生成先后是,this,直接调用this指向window,new关键字调用this指向实例本身;

  • 接着,行参初始化为undefined,实参赋值给行参;

  • 接着,提升,提升变量,提升方法; 如果有和行参同名的变量,行参被覆盖;

  • AO对象生成结束;将getName的任务队列入栈;

  • 任务队列中的代码执行;

  • 执行完出栈;

  • getName执行上下文销毁;同时销毁对GO的关联;

    image.png

六、作用域链 scope chain

作用域链在全局,指的就是GO对象,变量的获取,从GO中查找;
作用域在函数执行时,指的就是AO对象 + 上一级函数的AO对象引用 + 上上级函数的AO对象引用 ... + 直至GO对象;

变量访问就是按照这个作用域链查找,直至找不到;

参考

  1. 函数声明和函数表达式的区别
  2. JS执行机制