开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
函数 === 方法 ?
函数(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.
解析值的有效代码单元。或者说是计算功能的最小单位。
-
js代码 等价于 一组语句;
JavaScript applications consist of statements; -
一条语句 等价于 一组关键字 等价于 表达式/声明的组合;
A single statement isn't a keyword, but a group of keywords; -
表达式 包含
- 基本表达式
- 基本表达式和操作符(+、-、&&、 || 等)组成的复杂表达式
- 基本表达式
-
super this
-
new
-
function
-
()括号操作符
-
[]
-
{}
-
等
- 关键字 包含
- 声明变量 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只关乎行参;
二、函数声明提升
- 函数声明提升
- 函数表达式不提升
- 函数声明提升:可以在让函数在函数定义前被调用
- 通过函数声明定义的函数,等同于,定义函数 同时 将函数赋值给相同函数名字的变量,所以可以通过变量名调用。
// 表达式定义
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
以浏览器环境为例子:
预编译 --- GO
-
JS文件就是一个JS模块,js代码执行前会进行预编译,也可理解为生成一个GO对象;
-
GO对象包含浏览器内置的构造函数,Object、Array等、全局this、还有提升的变量和方法;
-
变量提升的先后顺序是var变量、方法;前文也提到,函数声明实际上就是将函数赋值给一个与函数名同名的变量,放在全局才能调用到方法;如果定义了相同的方法名和变量,在变量赋值之前访问变量,取到的是方法;
console.log(a); // [Function: a] function a(){} var a = 1 console.log(a); // 1
代码执行 --- 全局执行上下文 --- GEC
-
将预编译生成的GO对象入上下文栈;
-
执行上下文是一个栈数据结构,符合栈的先进后出的特点;
-
js引擎事件处理机制是单线程,这里只看简单的例子们不考虑异步任务;
-
全局任务队列入栈;
-
任务执行;
-
代码一行一行执行;
-
遇到函数执行;
-
函数执行的瞬间生成AO对象;
-
函数getName的任务队列入栈;
-
geName函数执行;
-
...遇到函数就入栈,函数执行完出栈,并且取消与上一季函数的关联;
-
getName函数执行完,getName销毁;
-
回到全局上下文栈;
-
... 继续往下执行;
函数执行上下文 --- FEC --- AO对象 active object
-
函数执行上下文栈,首先要保存上一级的作用域,就是GO,保存的是引用,不是复制;
-
函数执行的瞬间生成AO对象;
-
AO对象属性生成先后是,this,直接调用this指向window,new关键字调用this指向实例本身;
-
接着,行参初始化为undefined,实参赋值给行参;
-
接着,提升,提升变量,提升方法; 如果有和行参同名的变量,行参被覆盖;
-
AO对象生成结束;将getName的任务队列入栈;
-
任务队列中的代码执行;
-
执行完出栈;
-
getName执行上下文销毁;同时销毁对GO的关联;
六、作用域链 scope chain
作用域链在全局,指的就是GO对象,变量的获取,从GO中查找;
作用域在函数执行时,指的就是AO对象 + 上一级函数的AO对象引用 + 上上级函数的AO对象引用 ... + 直至GO对象;
变量访问就是按照这个作用域链查找,直至找不到;