JS 函数
- 函数的基本格式
- 函数的创建方式
- **函数参数传递**
- **函数作用域**
- 无块级作用域
- 函数的属性和方法
- 闭包
- **1. 定义和形成**
- **2. 私有变量**
- **3. 延长变量的生命周期**
- 4. 函数作为值
- 5. 闭包和内存泄漏
- 6. 闭包的应用
JavaScript(简称JS)是一种广泛使用的脚本语言,主要用于网页开发,但它也被用于各种非浏览器环境。在JavaScript中,函数是一种特殊的对象,用于封装可重复使用的代码块。以下是JavaScript中函数的基本格式和创建方式:
函数的基本格式
JavaScript函数的基本格式如下:
function functionName(parameters) {// 代码块
}
function
是创建函数的关键字。functionName
是函数的名称,它应该遵循标识符的命名规则。parameters
是函数的参数列表,多个参数之间用逗号分隔。在函数体内部,这些参数可以像变量一样使用。- 函数体(
// 代码块
)是实际执行的代码。
函数的创建方式
- 函数声明(Function Declaration)
这是最传统的方式,使用function
关键字。
function greet(name) {console.log('Hello, ' + name + '!');
}
函数声明会提升(hoisting),这意味着函数可以在声明之前被调用。
- 函数表达式(Function Expression)
函数也可以作为表达式被创建,通常赋值给一个变量。
var greet = function(name) { //匿名函数console.log('Hello, ' + name + '!');
};greet('Alice'); // 输出: Hello, Alice!
函数表达式不会提升,必须在调用之前定义。
- 箭头函数(Arrow Function)
ES6(ECMAScript 2015)引入了箭头函数,这是一种更简洁的函数写法。
const greet = (name) => {console.log('Hello, ' + name + '!');
};
greet('Alice'); // 输出: Hello, Alice!const greet = name => console.log('Hello, ' + name + '!'); //精简用法
greet('Bob'); // 输出: Hello, Bob!
如果函数体只有一条语句,可以省略花括号,并且隐式返回该语句的结果。
const greet = name => 'Hello, ' + name + '!';
箭头函数没有自己的this
、arguments
、super
或new.target
,这些值都由外围最近一层非箭头函数决定。
- 方法(Method)
在对象内部定义的函数,称为方法。
const person = {name: 'John',greet: function() {console.log('Hello, ' + this.name + '!');}
};
- 构造函数
使用new
关键字创建的函数,用于创建对象实例。
function Person(name) {this.name = name;
}const person = new Person('John');
- Function 构造函数
使用Function
构造函数动态创建函数。
const greet = new Function('name', 'console.log("Hello, " + name + "!");');
这种方法不常用,因为它可能导致安全问题,并且不利于JavaScript引擎优化。
函数参数传递
在JavaScript中,函数参数的传递方式主要有两种:按值传递和按引用传递。
- 按值传递(Pass by Value):
- 基本数据类型(如
Number
、String
、Boolean
、null
、undefined
和Symbol
)是通过值传递的。 - 当你将一个基本数据类型的值作为参数传递给函数时,实际上是传递了这个值的一个副本。
- 函数内部对这个参数的任何修改都不会影响到原始数据。
function modifyValue(value) {value = 10;
}let a = 5;
modifyValue(a);
console.log(a); // 输出 5,因为函数内部修改的是值的副本,不影响原始变量
- 按引用传递(Pass by Reference):
- 复杂数据类型(如
Object
、Array
、Function
)是通过引用传递的。 - 当你将一个对象作为参数传递给函数时,实际上是传递了这个对象**引用的副本,**但这个副本指向的是同一个内存地址。
- 函数内部对这个参数对象的属性进行修改,会影响到原始对象。
function modifyObject(obj) {obj.name = 'Kimi';
}let person = { name: 'Alice' };
modifyObject(person);
console.log(person.name); // 输出 'Kimi',因为函数内部修改的是对象本身
需要注意的是,虽然对象是按引用传递的,但是传递给函数的是引用的副本,这意味着你不能改变传递的对象引用本身(例如,将对象参数赋值为另一个新对象),只能修改对象的属性。
function changeObject(obj) {obj = { name: 'Bob' };
}let person = { name: 'Alice' };
changeObject(person);
console.log(person.name); // 输出 'Alice',因为函数内部修改的是引用的副本,不影响原始对象引用
在JavaScript中,没有真正的按引用传递,因为即使对于对象,传递给函数的也是引用的副本。这种传递方式有时被称为“按共享传递”或“按引用传递的副本”。
函数作用域
在JavaScript中,作用域的概念对于变量的访问和生命周期至关重要
全局作用域
在函数体外部定义的变量
在函数体内部定义的无var的变量 函数要先执行后输出
在任何位置都可以调用
// 全局变量
var globalVar = "global variable";// 全局函数
function globalFunction() {console.log("global function");
}// 在全局作用域中访问全局变量和函数
console.log(globalVar); // 输出 global
globalFunction(); // 输出:I am a global function// 在函数内部访问全局变量和函数
function accessGlobal() {console.log(globalVar); // 输出:global variableglobalFunction(); // 输出:global function
}accessGlobal()
局部作用域(函数作用域)
在函数内部使用var定义的
函数的参数
在函数内部可以调用
function myFunction() {// 函数内部的变量,只在函数作用域内可见var localVar = "local variable";// 函数内部的函数,只在函数作用域内可见function localFunction() {console.log("local function");}// 在函数内部访问局部变量和函数console.log(localVar); // 输出:local variablelocalFunction(); // 输出:local function
}// 尝试在全局作用域中访问函数内的局部变量和函数
// 这将导致错误,因为它们不在全局作用域中
// console.log(localVar); // ReferenceError: localVar is not defined
// localFunction(); // ReferenceError: localFunction is not definedmyFunction()
优先级
局部变量高于同名全局变量
参数变量高于同名全局变量
局部变量高于同名参数变量
内层函数可以访问外层函数局部变量,外层函数不可以访问内层函数局部变量。
无块级作用域
ES5中没有块级作用域,只有函数作用域,通过 var 声明的变量是函数作用域,并不是块级作用域。ES6中才引入块级作用域。
没有块级作用域意味着变量不会因为它们被声明在某个代码块(如if
语句、for
循环等)内部而局限于那个代码块。这会导致一些常见的问题,比如变量覆盖和变量提升。
1、 变量提升(Hoisting): 在JavaScript中,变量和函数声明会被提升到它们所在作用域的顶部。这意味着即使在代码中在声明之前使用了变量,它仍然可以被访问,因为它实际上在代码执行前就已经被“移动”到了顶部。
console.log(myVar); // 输出:undefined
var myVar = 10;
2、 没有块级作用域: 由于JavaScript没有块级作用域,var
声明的变量即使在if
语句或for
循环内部声明,也会被提升到包含它们的函数或全局作用域中。
if (true) {var x = 5;
}
console.log(x); // 输出:5
3、 变量覆盖: 如果没有块级作用域,变量可能会被意外覆盖。例如,在一个循环中,使用var
声明的变量可能会被多次覆盖,导致最终的值与预期不符。
for (var i = 0; i < 5; i++) {i = 10;
}
console.log(i); // 输出:10,而不是5
4、 let
和const
的引入: 为了解决这些问题,ES6引入了let
和const
关键字,它们提供了块级作用域。使用let
和const
声明的变量不会被提升,它们只在声明它们的代码块内部可见。
let x = 10;
if (true) {let x = 5; // 这个x只在if块内部可见
}
console.log(x); // 输出:10
5、let
和const
的暂时性死区(Temporal Dead Zone, TDZ): 与var
不同,let
和const
声明的变量在代码块的开始到声明的位置之间形成了一个“死区”,在这个区域内访问这些变量会导致ReferenceError
。
console.log(x); // ReferenceError: x is not defined
let x = 10;
6.const
的不变性: 使用const
声明的变量必须在声明时初始化,并且其值不能被重新赋值(对于基本数据类型)。
const x = 10;
x = 20; // TypeError: Assignment to constant variable.
了解这些概念对于编写更安全、更可预测的JavaScript代码非常重要。使用let
和const
可以帮助避免许多由于作用域引起的错误。
示例:
console.log(myFunction()); // 输出:Hello Worldfunction myFunction() {return "Hello World";
}
在这个例子中,尽管myFunction
函数在调用它的console.log
语句之后被声明,但由于提升,函数在调用时已经存在,所以可以正常工作。
函数表达式的“提升”
对于函数表达式,JavaScript解释器只会提升变量(包括函数名),但不会提升函数表达式的值(即函数体)。这意味着函数表达式必须在声明之后才能被调用。
示例:
console.log(myFunction()); // 输出:undefinedvar myFunction = function() {return "Hello World";};
}
在这个例子中,myFunction
变量被提升到了作用域的顶部,但是它被初始化为undefined
,因为函数表达式的值(函数体)没有被提升。因此,当尝试调用myFunction()
时,它仍然是undefined
,导致调用失败。
总结
- 函数声明的“提升”:函数声明的整个内容(包括函数名和函数体)都被提升到作用域的顶部。
- 函数表达式的“提升”:只有变量名被提升,函数表达式的值(函数体)没有被提升。
这就是为什么函数声明可以在声明之前被调用,而函数表达式则不能。这种差异是JavaScript作用域和提升机制的一个重要特点。
(function($){cat = {name:"mimi",age:"2"}$.ModuleB = cat;})(window);
THIS
在JavaScript中,this
关键字是一个非常重要的概念,它指向函数执行的上下文环境。this
的值取决于函数是如何被调用的,而不是在哪里定义的。
window.color = "blue";var obj = {color:"red",sayHello:function(){return this.color;}}console.log(this.color); //输出:blueconsole.log(obj.sayHello()); //输出:red
谁调用某个属性或方法,this就指向谁。
function a(){var user = "xiaoming"console.log(this.user); // 输出:undefinedconsole.log(this) // 输出:window{..}}a();
var o = {a:10,b:{a:12,fn:function(){console.log(this.a)}}}o.b.fn(); // 输出:12
var o = {a:10,b:{a:12,fn:function(){console.log(this.a) // 输出:undefinedconsole.log(this) // 输出:Window{..}}}}var j = o.b.fn; //只是调用,并没有执行j(); //最终还是在Window的作用域下执行
函数的属性和方法
在JavaScript中,函数是一等公民,这意味着函数可以像任何其他变量一样被传递和操作。函数对象有几个内置属性,其中包括arguments
、name
、length
和prototype
。
function sayHello(){}console.dir(sayHello)
console.dir(sayHello)
ƒ sayHello()
arguments: null
caller: null
length: 0
name: "sayHello"
prototype: {}
[[FunctionLocation]]: f03.html:10
[[Prototype]]: ƒ ()
[[Scopes]]: Scopes[1]
- arguments
arguments
是一个类数组对象,它包含了函数调用时传入的所有参数。这个对象在函数体内部可用,允许你访问所有参数,即使函数没有显式地定义足够多的参数。 (可以获得形参的数量)
javascript
function printArgs() {console.log(arguments); // 输出:[Arguments] { '0': 'foo', '1': 'bar' }for (let i = 0; i < arguments.length; i++) {console.log(arguments[i]);}
}printArgs('foo', 'bar');
- name
name
属性包含了函数的名称。这个名称是在函数被创建时赋予的,对于匿名函数,这个属性可能是空的。
function myFunction() {}
console.log(myFunction.name); // 输出:myFunctionconst anonymous = function() {};
console.log(anonymous.name); // 输出:(可能是空字符串或者"anonymous",取决于JavaScript引擎)
- length
length
属性表示函数期望接收的参数个数,即函数定义中形参的数量。(输出的是实参的数量)
function myFunction(a, b, c) {}
console.log(myFunction.length); // 输出:3
- prototype
prototype
属性是一个对象,包含了可以由通过该构造函数创建的对象继承的属性和方法。这是JavaScript原型链的基础。
function MyConstructor() {}
console.log(MyConstructor.prototype); // 输出:MyConstructor的原型对象const instance = new MyConstructor();
console.log(instance.__proto__ === MyConstructor.prototype); // 输出:true
prototype
属性在创建构造函数时特别有用,因为它允许你定义所有实例共享的属性和方法。当你创建一个新对象时,这个对象的内部属性(__proto__
)会指向其构造函数的prototype
属性,从而实现原型继承。
call() , bind() , apply() 这三个函数都可以改变函数调用时 this的指向,即指定this的值。
- call():
call()
方法用于在指定的this
值和参数的情况下 调用一个函数,即调用一个具有给定this
值的函数,以及单独提供的参数。
var n = 123;function say(){console.log(this.n);//此时this的作用域为Window}var o = {n:3453}//冒充o对象 改变say函数内部的 this指向,此时this的作用域为owindow.say.call(o) // 输出:3453
传参:
function a(name,age){console.log(name,age);console.log(this);return 123;}obj = {}a.call(obj,"kimi","2")//指定了 this 的值为 obj,而调用的函数是a
参数传递:
函数 a
被调用时,name
参数被赋值为 "kimi"
,age
参数被赋值为 "2"
。
this 值:
由于使用了 call()
方法,并指定了 this
值为 obj
,所以在函数 a
内部,this
将指向 obj
对象。
控制台输出:
第一个 console.log(name, age)
将输出:kimi 2
,因为这两个值是作为参数传递给函数的。
第二个 console.log(this)
将输出:{}
,因为 this
被设置为 obj
,而 obj
是一个空对象。
返回值:
函数 a
返回 123
,但由于没有变量接收这个返回值,所以它不会在代码中产生进一步的影响。
function sum1(num1, num2){return num1+num2;}function sum2(num1, num2){return sum1.call(sum2,num1,num2);}console.log(sum1(5,5)) // 输出:10console.log(sum2(5,5)) // 输出:10
- apply()和call()基本用法一致,但是传递参数是接收的是数组,即调用一个具有给定
this
值的函数,以及作为一个数组(或类数组对象)的参数。
var name = "jack";function fn(){console.log(this.name);}var obj = {name:"anndy"}fn.apply(obj)
传参:
function sum1(num1, num2){return num1+num2;}function sum2(num1, num2){return sum1.apply(sum2,[num1,num2]); // !!!!!}console.log(sum1(5,5)) // 输出:10console.log(sum2(5,5)) // 输出:10
- bind()
var name = "qqtang"function say(){console.log(this.name);}var obj = {name:"milk"}say.bind(obj)();//say.bind(obj)返回的是一个函数,所以要加()再执行一下
上面代码的执行原理:
function fn(){return function(){}console.log(123);}fn()();
传参:
var name = "qqtang"function say(name,age){console.log(this.name);console.log(name,age);}var obj = {name:"milk"}say.bind(obj,"ross",21)()//或者
var name = "qqtang"function say(name,age){console.log(this.name);console.log(name,age);}var obj = {name:"milk"}say.bind(obj,"ross")(21)// bind()的科里化特性
bind()
函数的一个特性是“柯里化”(Currying),这是指将多参数的函数转换成一系列使用一个或两个参数的函数。bind()
方法可以部分应用一个函数,即预先填充一个或多个参数,然后返回一个新函数,这个新函数可以接收剩余的参数。
总结:
call()
:当你需要立即执行函数,并且参数列表已知时使用。
apply()
:当你需要立即执行函数,但参数列表是动态的或者存储在数组中时使用。
bind()
:当你需要创建一个新的函数,其 this
值被预设,或者需要预设参数时使用。bind()
通常用于事件处理和回调函数中,以确保 this
的值是预期的对象。
这三个方法都是 JavaScript 函数原型上的方法,所以它们可以被任何函数使用。
闭包
1. 定义和形成
闭包是由函数以及创建该函数时所处的词法环境(作用域)组合而成的。当一个函数在其外部函数之外被引用时,就会形成闭包。
闭包是一种概念但是在]S里面我们可以通过具体的程序来表达闭包的这种概念
闭包和函数与作用域有关系,闭包就是一个函数内部的函数,内层的函数可以访问到外层的函数的作用域中的变量
闭包可以将一个变量驻留到内存中不会被垃圾回收机制回收,因为某个变量还被引用所以不会回收
可以让外部访问到局部变量,也可以模拟一个私有作用域。
闭包(Closure)是JavaScript中一个非常重要的概念,它指的是一个函数能够访问其定义时的作用域链,即使在其定义的作用域之外执行。闭包通常用于创建私有变量和封装函数,:
function outerFunction() {var outerVar = "I am outer";return function innerFunction() {console.log(outerVar);};
}const myClosure = outerFunction();
myClosure(); // 输出:I am outer
在这个例子中,innerFunction
能够访问 outerFunction
的作用域中的 outerVar
变量,即使 innerFunction
在 outerFunction
外部被调用。
2. 私有变量
闭包可以用来创建私有变量,这些变量只能在闭包内部被访问和修改,从而隐藏实现细节。
function createCounter() {let count = 0;return function() {count += 1;return count;};
}const counter = createCounter();
console.log(counter()); // 输出:1
console.log(counter()); // 输出:2
var Counter = function(){var privateCounter = 0;function changeby(n){privateCounter += n;}return {increment:function(){changeby(1);},decrment:function(){changeby(-1)},value:function(){return privateCounter; }}}();console.log(Counter.value()) //输出:0Counter.increment()console.log(Counter.value()) //输出:1Counter.decrment()console.log(Counter.value()) //输出:0
3. 延长变量的生命周期
闭包可以延长变量的生命周期,即使外部函数已经执行完毕,闭包中的变量仍然可以被访问。
function dataKeeper() {var importantData = "This data is important";window.leakData = function() {return importantData;};
}dataKeeper();
console.log(window.leakData()); // 输出:This data is important
4. 函数作为值
在JavaScript中,函数是一等公民,这意味着函数可以像任何其他值一样被传递和返回,这是形成闭包的基础。
function factory() {var res = [];for (var i = 0; i < 5; i++) {res[i] = function() {return i;};}return res;
}const array = factory();
for (var i = 0; i < array.length; i++) {console.log(array[i]()); // 输出:5, 5, 5, 5, 5
}
var arr = [];for(var i = 1 ; i<=5 ; i++){arr[i-1] = function(num){return function(){return num};}(i); //传参,把i传给num}for(var j = 0 ; j<arr.length ; j++){console.log(arr[j]());} //输出:1, 2, 3, 4, 5
5. 闭包和内存泄漏
由于闭包会持续访问外部函数的变量,这可能导致内存泄漏,尤其是在循环和DOM元素事件处理程序中。
for (var i = 0; i < 100; i++) {document.body.addEventListener('click', (function(index) {return function() {console.log(index);};})(i));
}
在这个例子中,每个事件监听器都创建了一个闭包,这些闭包持续引用了循环变量 i
,导致 i
的值无法被垃圾回收。
6. 闭包的应用
闭包在JavaScript中有着广泛的应用,包括模块模式、事件处理程序、回调函数等。
- 模块模式:使用闭包来实现模块的私有变量和公共接口。
- 事件处理程序:在事件处理程序中使用闭包来访问和修改DOM元素的状态。
- 回调函数:在异步编程中使用闭包来保存和传递状态。
理解闭包是掌握JavaScript高级特性的关键,它不仅涉及到函数的执行,还涉及到内存管理和代码的封装。