函数
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
函数是第一等公民
函数是本身也是一种值,类似字符串、数值、布尔。
函数的声明
- 语句式声明
//函数语句式声明
function add( a, b ){
return a+b;
}
- 表达式声明
//函数表达式声明
var add = function( a, b ){
return a+b;
}
- 构造函数声明
var add = new Function( 'a', 'b','return a + b' );
示例
//语句式声明
function add( a, b ){
return a+b;
}
console.log( add(1,5) );//6
//表达式声明
var add = function( a, b ){
return a+b;
}
console.log( add(1,5) );//6
//构造函数声明
var add = new Function( 'a', 'b','return a + b' );
console.log( add(1,5) );//6
语句式声明和表达式声明有什么区别?
- 语句式声明会自动提升
//函数会自动提升,不会报错
console.log( add(1,5) ); //6
//语句式声明
function add( a, b ){
return a+b;
}
console.log( add(1,5) ); //6
- 表达式声明不提升
//不会提升,致命错误
console.log( add(1,5) ); //Uncaught TypeError: add is not a function
//表达式声明
var add = function( a, b ){
return a+b;
}
console.log( add(1,5) ); //6
构造函数声明可以用来解析字符串表达式,类似eval
console.log( eval('5+6*2') );//17
var fn = new Function( 'return 5+6*2' );
console.log( fn() );//17
函数的重复声明
代码是从上到下执行,后面的函数覆盖前面的函数,注意语句式声明函数会提升到代码顶部
//var 定义 会覆盖
var a = '111';
var a = '222';
console.log( a ); //会覆盖 222
//let 定义 不会覆盖
let b = 'aaa';
let b = 'bbb';
console.log( b );//报错,重定义错误
//const 定义 不会覆盖
const c = 'true';
const c = 'false';//报错,重定义错误
//函数 重定义 会覆盖
function add(){
console.log('hello');
}
var add = function(){
console.log('您好');
}
function add(){
console.log('fine');
}
add(); //因为语句式声明会提升,表达式不提升,所以输出您好
圆括号运算符,return 语句和递归
- 圆括号
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。
function add(x, y) {
return x + y;
}
//调用函数加圆括号,在括号之中加入参数
add(1, 1) // 2
return
语句
return
返回并且跳出函数,return
是函数内部的语句。
return
和 break
的区别
function echo(){
for(var i=0;i<10;i++){
if(i>5){
return i; //跳出函数
break; //跳出循环
}
}
return 'hello';
}
console.log( echo() ); //6
return
是函数内部语句,必须有函数包裹
for( var i=0; i<10; i++ ){
console.log( i );
if(i>5){
return;
}
}
//报错,Uncaught SyntaxError: Illegal return statement
- 函数递归
函数可以调用自身,这就是递归,递归一定要有个结束值(return
),不然会形成死循环。
//因为没有结束条件,所以这是一个死循环
function fn( n ){
console.log( n );
fn( n+1 );
}
fn( 0 );//0 1 2 3 .....
//降序
function desc( n ){
console.log( n );
if( n == 0 ) return;
desc( n-1 );
}
desc( 5 );//5 4 3 2 1 0
//升序
function asc( n ){
console.log( n );
if( n == 5 ) return;
asc( n + 1 );
}
asc( 0 );//0 1 2 3 4 5
//阶乘
function fn( n ){
// console.log( n );
if( n == 0 ) {
return 1;
}else{
return n * fn( n-1 );
}
}
//斐波那契数列
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num - 2) + fib(num - 1);
}
fib(6) // 8
函数是第一等公民
JavaScript
语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。
- 可以把函数赋值给变量
var add = function( a, b){
return a+b;
}
- 可以把函数赋值给对象的属性
var obj = {
name: '张三',
getAge: function(){
return 30;
}
}
- 可以当作参数传入其他函数(作为参数时不能加括号)
//1. 将函数作为参数,并提取出来声明
function add( a, b ){
console.log( a ); // function(){ }
console.log( b ); // 5
console.log( a() + b );//要加括号 需要执行
}
function fn(){
return 10;
}
add( fn, 5 );//15
//2. 将函数作为参数声明
function add( a, b ){
console.log( a() + b );
}
add( function(){
return 10;
}, 5 );// 15
函数作为参数传入到函数内部,我们也叫这个函数(参数)回调函数
//仿照数组实例forEach
function forEach( arr, fn ){
for(var i=0;i < arr.length;i++){
fn( arr[i], i, arr );
}
}
//调用
var arr = ['a','b','c','d'];
forEach( arr, function( item, index, arr){
console.log( item, index, arr );
});
//仿照数组实例fliter
function filter( arr, fn ){
var newArr = [];
for(var i=0; i < arr.length; i++ ){
var result = fn( arr[i], i, arr );
if( result ){
newArr.push( arr[i] );
}
}
return newArr;
}
var newData = filter( data, function( item, index, arr ){
return item.name=='张三';
});
console.log( newData );
函数名的提升
JavaScript
引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
f();
function f() {}
变量提升
//1
console.log( a ); // 报错,未定义
//2
console.log( a ); // undefined
var a;
//3
console.log( a ); // undefined
var a = 10;
//4
console.log( a ); // undefined
var a = 10;
console.log( a ); // 10
变量提升面试题
function foo( x ){
if( x > 100 ) {
var tmp = x - 100;
}
console.log( tmp );
}
foo( 50 ); // undefined
//以上代码变量会提升,等同于
function foo( x ){
var tmp;
if( x > 100 ) {
tmp = x - 100;
}
console.log( tmp );
}
不能在条件语句中声明函数
不要在条件语句是声明函数,以下写法不推荐
var foo = false;
if ( foo ) {
function fn() {
console.log('hello');
}
}
fn();//报错 fn is not a function
函数的属性和方法
name
属性
//语句式声明
function f1() {
}
f1.name // "f1"
//表达式声明
var f1 = function add(){
}
f1.name // "add"
递归函数解耦
//递归
function desc( n ){
console.log( n );
if(n==0)return;
arguments.callee( n-1 ); // arguments.callee 存在兼容性问题
}
desc( 10 );
//替换上面的方法,兼容性更好
var desc = function fn( n ){
console.log( n );
if( n == 0 ) return;
fn( n-1 );
}
desc( 5 );
length
属性
函数的长度指的是形参的个数
function fn( a, b, c, d ){
console.log( fn.length );
}
console.log( fn.length ); // 4
fn(); // 4
toString()
函数的toString
方法返回一个字符串,内容是函数的源码。
function add( a, b ){
/*
这是一个
多行注释
*/
return a+b;
}
console.log( add.toString() );
函数作用域
作用域(scope)指的是变量存在的范围
js两种作用域:
- 全局作用域(全局变量):
变量在整个程序中一直存在,所有地方都可以读取,关闭当前窗口,才能释放内存。
<script>
var str = '张三';//全局变量
</script>
- 函数作用域(局部变量):
变量只在函数内部存在
注意:在函数内部使用
var
关键字声明的变量是局部变量
function fn(){
var str = '李四';
}
fn();
console.log( str ); //报错 str is not defined
注意:在函数内部省略
var
关键字,会自动提升为全局变量。
function fn(){
str = '李四';
}
fn();
console.log( str ); // 李四
<script>
var str = '张三';//全局变量
function fn(){
var age = 20; //局部变量
}
</script>
变量提升面试题 1
fn(); // undefined 20
var str = '张三';
function fn(){
var age = 20;
console.log( str, age);
}
变量提升面试题 2
function fn(){
if ( false ) {
var x = 5;
}
console.log(x);
}
fn(); // undefined
变量提升面试题 3
//代码从上到下执行
function x(){
console.log(a);
a = 10;
}
x();
console.log( a ); // Uncaught ReferenceError: a is not defined
变量提升面试题 4
function x(){
console.log(a);
var a = 10;
}
x(); // undefined
console.log( a ); // Uncaught ReferenceError: a is not defined
函数作用域面试题 1
var str = '张三';
function fn(){
var age = 20;
var str = '李四';
console.log( str, age);
}
fn(); // 李四 20
console.log( str ); // 张三
函数作用域面试题 2
var str = '张三';
function fn(){
var age = 20;
str = '李四';
console.log( str, age);
}
fn(); // 李四 20
console.log( str ); // 李四
函数作用域面试题 3
function one( ){
var str = '张三';
function two(){
var age = 20;
console.log( str );
}
two();
console.log( age );
}
one(); // 报错
函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时(调用时)所在的作用域无关。
情况 1
var a = 1;
//声明时作用域
var x = function(){
console.log(a);
}
function f() {
var a = 2;
//运行时(调用时)作用域
x();
}
f();//1
情况 2
var a = 1;
//声明时作用域
var x = function(){
var a = 5;
console.log(a);
}
function f() {
var a = 2;
//运行时(调用时)作用域
x();
}
f();//5
情况 3
//声明时作用域
var x = function(){
console.log(a);
}
function f() {
var a = 2;
//运行时(调用时)作用域
x();
}
f(); // a is not defined
情况 4
//声明时作用域
var x = function(){
console.log(a);
a = 10;
//注意没有使用var关键字 则没有优先提升权限
}
function f() {
var a = 2;
//运行时(调用时)作用域
x();
}
f(); // Uncaught ReferenceError: a is not defined
函数闭包
在函数内部返回函数,返回的函数调用了父函数的局部变量,这样形成了闭包。
- 闭包可以将函数内部的局部变量暴露给外部,是函数内部和外部的桥梁
- 闭包会导致函数内部的局部变量无法释放,不被垃圾回收站回收。
闭包的缺点:闭包因为会使局部变量内存无法及时释放,如果使用太多会增加内存开销,带来性能隐患。
function one(){
var name = '张三';
function two(){
console.log( name );
}
return two;
}
var fn = one();
fn();
闭包经典面试题 1
function bb(){
var num = 999;
return function(){
num++;
console.log( num );
}
}
var fun = bb();// function(){}
fun(); // 1000
fun(); // 1001
参数
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。
function square(x) {
return x * x;
}
参数可以省略,从后向前省略,形参和实际参数一一对应。
运行时无论提供多少个参数,或者不提供参数都不会报错,省略的参数值是undefined
函数的length
只和形式参数个数有关,和实际参数无关
没有办法只省略靠前的参数,而保留靠后的参数,从后向前省略。
形参的作用域是函数作用域,也就是局部变量
//1 使用条件语句
function add( a, b, c){
if(a==undefined) a =0;
if(b==undefined) b =0;
if(c==undefined) c =0;
return a + b + c;
}
console.log( add( 1, 2 ) );
//2 使得短路法则
function add( a, b, c){
a = a || 0;
b = b || 0;
c = c || 0;
return a + b + c;
}
console.log( add( 1, 2 ) );
//3 默认参数 es6
function add( a = 0, b = 0, c = 0){
return a + b + c;
}
console.log( add( 1, 2 ) );
函数参数传递方式
- 参数为原始类型(数值、字符串、布尔、undefined、null)传递方式是传值传递。
- 参数为引用类型(对象、数组)传递方式是传址传递。
比较下面两道题,注意区分:
//函数内部没有加var关键字,认为全局变量
var p = 2;
function f() {
p = 3;
}
f();
console.log( p );//3
//因为p是形参,而它是直接拷贝 2,内部覆盖它的值为3 不会影响到全局变量p
var p = 2;
function f( p ) {
p = 3;
}
f( p );
console.log( p );//2
数组是传址传递,所以在数组内部会影响到外面传入的数组
var arr = ['苹果','桔子'];
function f( arr ) {
arr[1] = '香蕉';
}
f( arr );
console.log( arr );//['苹果','香蕉']
解决办法,就是复制一个新数组
//使用es6扩展运算符生成新数组
var arr = ['苹果','桔子'];
function f( [...arr] ) {
arr[1] = '香蕉';
}
f( arr );
console.log( arr );//['苹果','桔子']
//使用concat生成新数组
var arr = ['苹果','桔子'];
function f( arr ) {
arr = [].concat( arr );
arr[1] = '香蕉';
}
f( arr );
console.log( arr );//['苹果','桔子']
对象因为也是引用类型,所有在函数内部修改对象时会改变外面的对象
var obj = {
name: '张三',
info: {
age: 20
}
};
function f( obj ) {
obj.info.age = 30;
}
f( obj );
console.log( obj );//年龄会被该成30
同名参数
如果有同名的参数,则取最后出现的那个值。
即使后面的a没有值或被省略,也是以其为准。
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
arguments
参数集合对象
由于 JavaScript
允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments
对象的由来。
arguments
是一个类数组对像,它包含了数组所有参数。
arguments
对象包含了函数运行时的所有参数(实际参数)
arguments.callee
指向的是函数本身
function max(){
var args = arguments;
var arr = [];
if( args.length > 1 ){
arr = Array.from( args );
}else{
if( typeof args[0] =='string' ){
arr = args[0].split('');
}else{
arr = args[0];
}
}
arr = arr.filter(function(item){
return isNaN(item) == false;
})
var max = arr[0];
for(var i=0;i<arr.length;i++){
if ( max < arr[i] ) max = arr[i];
}
console.log( max );
}
max('230a1');
max(2,'a',30,1);
max([2,'a',13,1]);
经典面试题:多种办法单击获取索引值
<ul>
<li>首页</li>
<li>关于我们</li>
<li>产品中心</li>
</ul>
<script>
var lis = document.querySelectorAll('li');
//1. es6 块级作用域实现
for(let i = 0; i < lis.length; i++ ){
lis[i].onclick = function(){
console.log( i );
}
}
//2.将索引值缓存到对象上,然后再到对象自身去取
for(var i = 0; i < lis.length; i++ ){
lis[i].index = i;
lis[i].onclick = function(){
console.log( this.index );
}
}
//3. 使用闭包来缓存索引值
for(var i = 0; i < lis.length; i++ ){
function fn( j ){
lis[j].onclick = function(){
console.log( j );
}
}
fn( i );
}
//4. 使用数组的indexOf方法获取索引值
var arr = Array.from( lis );
for(var i = 0; i < lis.length; i++ ){
lis[i].onclick = function(){
console.log( arr.indexOf( this ) );
}
}
</script>
立即调用的函数表达式 IIFE
圆括号()是一种运算符,跟在函数名之后,表示调用该函数。
比如,print()
就表示调用print
函数。
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
//官方建议
( function(){ /* code */ } )();
( function(){ /* code */ }() );
示例
+function( a, b ){
console.log(a+b);
}(2,3);
(function( a, b ){
console.log(a+b);
})(2,3);
(function( a, b ){
console.log(a+b);
}(2,3));
立即执行函数用途:
- 不必为函数命名,避免了污染全局变量
- IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
用js写原生插件或者组件,我们习惯用IIFE封装它,避免污染全局变量。
(function(){
var iAlert = function(){
console.log('我是警告框插件!');
}
//将私有的变量注册为全局变量
window.iAlert = iAlert;
}());
//调用插件
iAlert();
定义组件时,组件默认参数通常采用覆盖替换法实现
//默认参数
var oldObj = {
page: 1,
perpage: 5
}
//用户参数
var newObj = {
page: 3,
perpage: 10
}
//合并默认参数和用户参数
var obj = Object.assign( {}, oldObj, newObj);
// var obj = { ...oldObj, ...newObj };
console.log( obj );
js 严格模式
默认是比较宽松的模式
如何设置严格模式?
全局设置
<script>
//设置严格模式
'use strict'
</script>
函数内设置
<script>
//函数内设置严格模式
function fn(){
'use strict'
}
</script>
严格模式有那些要求(部分)?
- 声明变量需要使用关键字
var
、let
、const
,否则报错
'use strict'
str = '张三';
console.log(str); //报错
- 不允许函数的形参重名
'use strict'
function x(p1, p1) {
} //报错
- 不允许删除一个不允许删除的属性
'use strict';
delete Object.prototype; //错误
- 变量名不能使用
arguments
字符串
'use strict';
var arguments = 10;
console.log( arguments );
- 由于一些安全原因,在作用域
eval()
创建的变量不能被调用
'use strict';
eval ('var x = 2');
alert (x);
- 禁止this关键字指向全局对象。
'use strict';
function fn(){
console.log(this);
}
fn();//本来是指向windows,但在严格模式下不允许,返回undefined
- 不允许删除变量或对象。
'use strict';
var x = 3.14;
delete x;
使用new Function
将字符串当作命令执行。
语法
new Function('参数1','参数2','...','函数内部返回');
最后一个参数始终是函数内部返回代码
var jsonp = 'foo({"id": 42})';
// foo是函数的参数,它是一个函数(回调函数)。
var f = new Function( 'foo', jsonp );
// 相当于定义了如下函数
// function f(foo) {
// foo({"id":42}); //回调函数
// }
//调用
f(function (json) {
console.log( json.id ); // 42
})
jsonp原理
为了便于客户端使用数据(比如远程数据),逐渐形成了一种非正式传输协议,进行数据传送,这种方式我们叫JSONP
跨域,该协议的一个要点就是允许服务端返回一个回调函数,这个回调函数包裹这数据,这样客户端就可以方便的取到服务器返回的数据。
jsonp
主要原理是利用js回调函数机制实现一种非正式跨域。
- 远程服务端返回一个函数,该函数包裹数据
//服务端
//假设以下 http://www.jx-tongan.com/vueapi/hello.js 的内容
fn({
name:'张三',
age: 20,
habby: ['钓鱼','游泳']
});
- 客户端通过回调函数的形参获取到服务端返回的数据
<script>
//客户端
//服务端的数据在形参obj上
function city( obj ){
console.log( obj );
}
</script>
<script src="js/city.js"></script>