JS: Accumulate from 0系列之function

作者 Kylewh 日期 2017-03-08
JS
JS: Accumulate from 0系列之function

引用类型之-Function

声明

//函数声明会提前
var result = add(1,2,3,4,5,6,7,8,9);
console.log(result); //45
function add() {
var aArgs = [].slice.call(arguments);
return aArgs.reduce((a,b) => a+b);
}
//运用函数表达式则不会进行提前
console.log( _add(1,2,3,4) ); //"TypeError: _add is not a function
var _add = function () {
var aArgs = [].slice.call(arguments);
return aArgs.reduce((a,b) => a+b);
}

return&返回值

如果不显式返回值,那么这个函数的返回值默认为undefined

function cal (num1,num2, calType) {
switch (calType) {
case 'add':
return num1+num2;
break;
case 'minus':
return num1-num2;
break;
default:
return;
break;
}
}
//这时我们输入不在上列判断情况里的type
console.log( cal(1,2,'times') ); //undefined

任何时候一旦function内部遇到return,函数立即退出。

function fn( command ) {
if(command === 'stop'){
return;
}
return arguments.callee.name + ' is running...';
}
console.log( fn() ); //"fn is running..."
console.log( fn('stop') ) // undefined

Arguments

记住一点,形参和arguments对象的元素是同步的,且它们的内存空间是独立的,如果没有传入参数,arguments如同定义了却没有初始化一样,值是undefined.

关于函数内部的argumens对象我们有两项需要知道:

  • arguments.callee: 指针,指向拥有这个arguments对象的函数(本身),作用是可以对函数进行解耦
//我们先写一个简陋版factorial函数
function factorial(num) {
if( num <=1 ) {
return 1;
} else {
return num * factorial(--num);
}
}
//上面的函数存在一个问题,如果我们想复用这个函数,但是又必须得修改函数名,那么我们还必须修改内部的函数名,所以这是一个紧密耦合的函数,为了让其解耦,我们可以使用arguments.callee
function goodFactorial(num) {
if( num <=1 ) {
return 1;
} else {
return num * arguments.callee(--num);
}
}
  • arguments.callee.caller: 保存着调用当前函数的函数的引用,如果在全局作用域里调用当前函数,它的值为null.
function outer(){
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();
/* function outer(){
inner();
*/ }

length

function本质是对象,它也具有length属性,它的数值是由function声明时的形参个数决定的.

function fn(a,b,c) {
return a+b+c;
}
console.log(fn.length); // 3

注意这种特殊情况,rest参数下,此参数不计入length.

function _fn(firstArgs, ...args) {
return [].slice.call(arguments);
}
console.log(_fn.length); //1

我们还可以利用这个属性进行参数检测

//我们先在function原型上注入一个before函数,用来注入前置检测函数。 这是面向切面编程的思想(AOP),后面总结设计模式的时候会提到。
Function.prototype.before = function (beforeCheckFn) {
var _this = this;
return function(){
if(beforeCheckFn.apply(this,arguments)){
return _this.apply(this, arguments);
}
}
}
//我们的主函数,接受两个参数
function printTwoNum (num1, num2) {
[].slice.call(arguments).forEach((a)=>(console.log(a)));
}
//我们的前置检测函数,如果接受的函数参数不等于形参个数,就会抛出错误
function checkArgs (num1,num2) {
if( arguments.length !== arguments.callee.length) {
throw new Error('The amounts of arguments is not allowed.');
return false;
}
return true;
}
let newPrint = printTwoNum.before(checkArgs);
newPrint(1,2); // 1, 2
newPrint(1,2,3); // "Error: The amounts of arguments is not allowed.

没有重载

function add (num1) {
return num1 + 100;
}
function add (num1) {
return num2 + 200;
}
//后者会覆盖前者
//要模拟重载,思路是对传入的参数进行判断,然后进行相应的操作。

这里涉及到一个惰性函数的问题,如果在当前开发环境下我们需要进行环境监测从而判断函数如何执行(多见于兼容性监测),那么在执行环境稳定不变的前提下,我们不必每次都进行判断,我们可以在第一次判断时就给函数定性,从而在之后的调用中都直接执行适合当前环境的函数。

//通过这种模式可以提高函数的效率,不必每次都进行检测。
var addEvent = function(ele, type, handler) {
if(window.addEventListener) {
addEvent = function (ele, type, handler) {
ele.addEventListener(type, handler, false);
};
}else if (window.attachEvent) {
addEvent = function (ele, type, handler) {
ele.attachEvent('on' + type, handler);
};
}
addEvent(elem, type, handler);
}

this

函数内部的this指向函数的执行上下文,通俗的说就是函数被谁调用就指向谁。

  • 作为对象的方法调用
//
var obj = {
a: 1,
getA: function () {
console.log(this === obj, this.a);
}
}
obj.getA();
  • 作为普通函数被调用
window.name = 'globalName';
function getName() {
console.log(this.name);
}
getName();
//注意这种情况
var myObject = {
name: 'kylewh',
getMyName: function () {
return this.name;
}
}
var getMyName = myObject.getMyName; //此时相当于在window全局环境下声明了一个getName函数,这里的this不再存在于myObject的作用域里
console.log(getMyName()); //globalName
//strict 模式下, 普通函数调用下的this不再指向全局对象,而是undefined
function func() {
"use strict"
console.log(this);
}
func(); //undefined
  • 构造器调用
function Construc() {
this.name = 'kylewh';
} //隐式的返回了一个对象,this指向这个对象
var ooobj = new Construc();
console.log(ooobj.name);
//如果显式的返回一个对象,那么this会指向这个对象
function _Construc() {
this.name = 'kylewh';
return {
name: 'wenhao'
}
}
var oooobj = new _Construc();
console.log(oooobj.name); //wenhao

我们还可以在函数内部使用return this来达成链式调用的效果.

Function.prototype.addMethod = function (name, fn) {
this[name] = fn;
return this; //方便链式添加
}
var method = function () {};
method
.addMethod('checkName', function () {
console.log('验证名字');
return this; //方便链式调用
}).addMethod('checkEmail', function () {
console.log('验证Email');
return this;
}).addMethod('checkPhone', function () {
console.log('验证手机');
return this;
});
console.dir(method);
method.checkName().checkEmail().checkPhone();
//验证名字, 验证Email, 验证手机

call & apply

这两个函数的作用本质上都是改变函数的执行上下文,并传入执行参数,而apply是可以支持以数组的的形式传入参数的。

window.color = "red";
var o = {
color: 'blue'
};
function sayColor() {
alert(this.color);
}
sayColor(); //red
sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //blue
function _add(num1 , num2) {
console.log ( num1 + num2 );
}
var num = [10,20];
_add.apply(null, num); //30

bind

也是用来改变函数的执行上下文,当时返回的是绑定传入上下文之后的函数。

var test = {
name: 'kylewhtest'
};
var fn2 = function (num1, num2, num3) {
console.log(this.name + num1 + num2 + num3);
}.bind(test);
fn2(1, 2, 3); //kylewhtest123
//bind可以只传入部分参数
var fn2 = function (num1, num2, num3) {
console.log(this.name + num1 + num2 + num3);
}.bind(test,1,2);
fn2(3); //kylewhtest123

一些例子

  • 以下代码输出什么?
var john = {
firstName: "John"
}
function func() {
alert(this.firstName + ": hi!")
}
john.sayHi = func //this指向函数执行环境,即john
john.sayHi() // alert John :"hi!"
  • 下面代码输出什么,为什么?
func() //执行函数,当前执行环境是window,alert [object Window]
function func() {
alert(this)
}
  • 下面代码输出什么?
document.addEventListener('click', function(e){
console.log(this); //函数执行环境 -> document; this -> document
setTimeout(function(){
console.log(this); //函数执行环境 -> 全局(Winodow); this -> Window
}, 200);
}, false);
  • 下面代码输出什么,why?
var john = {
firstName: "John"
}
function func() {
alert( this.firstName )
}
func.call(john) //call改变执行环境,this指向john,this.firstName = "John"
  • 以下代码有什么问题,如何修改?
var module= {
bind: function(){
$btn.on('click', function(){
console.log(this) //this指向eventTarget,在此即被点击的DOM元素($btn)
this.showMsg(); //dom元素上并没有挂载这个函数,无法执行
})
},
showMsg: function(){
console.log('hi');
}
}

修改版:

var module= {
bind: function(){
let _this = this; //将函数执行上下文缓存为一个变量
$btn.on('click', function(){
console.log(this)
_this.showMsg();
})
},
showMsg: function(){
console.log('You clicked the button!');
}
}
module.bind();
//点击后显示: You clicked the button!

综合运用

根据前面的call和apply,我们可以先模拟一个简单版本的bind,不考虑部分参数传入的问题:

Function.prototype.bind = function (context) {
var _this = this;
return function () {
return _this.apply(context, arguments);
}
}

再加上前后参数拼接在一起,完整版:

Function.prototype.bind = function () {
var _this = this;
var context = [].shift.call(arguments);
var args = [].slice.call(arguments);
return function(){
var newArgs = [].concat.call( args, [].slice.call(arguments) );
return _this.apply(context, newArgs);
}
}
//Test
var test = {
name: 'kylewhtest'
};
var fn2 = function (num1, num2, num3) {
console.log(this.name + num1 + num2 + num3);
}.bind(test,1,2);
fn2(3); //kylewhtest123

关于参数

ES6新特性:可以在函数声明时传入默认参数

function sayName(name = 'kyle') {
console.log(`My name is ${name}`);
}
sayName(); // My name is kyle!
//还可以将一个函数传入!
function sayName(name = 'kyle', callback = function(){}) {
console.log(`My name is ${name}`);
//我们不需要去判断if(callback)了
callback();
}

又是烦人的this

先看下面的代码:

var fn = function(){
console.log(this.a);
}
function fn1 (){
console.log(this.a);
}
var obj = {a:2, fn:fn, fn1:fn1},
a = 'Surprise!! >_<';
obj.fn(); //2
obj.fn1(); //2
var bar = obj.fn;
bar(); //Surprise!! >_<
//同样,看看回调
function doFn(fn){
fn();
}
doFn(obj.fn); //参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
setTimeout(obj.fn1, 1000); // Surprise!! >_<
setTimeout(function(){
obj.fn1(); //2
},2000)

出现这种问题的原因本质是bar()obj.fn()obj.fn1obj.fn1()的区别:

请看着这条语句: var bar = obj.fn, 现在开始你问我答环节。

  • 问: 等号的出现意味着什么!?
  • 答: 传递引用!
  • 问: Nice! 那引用的对象是谁!?
  • 答: fn 这个函数
  • 问: 好,那么执行的方式是怎样的!?
  • 答: 独立调用!
  • 问: 独立调用时,存在对象上下文吗!?
  • 答: 没有….额…等等…. window算吗?
  • 问: Excelletn baby! 就是window. 那this指向谁?
  • 答: window嘛!!
  • 问: 再看看延迟函数里的回调传入方式,传入obj.fn1时是在干什么?
  • 答: 传递引用!!
  • 问: 那匿名函数里的obj.fn1()是在干什么?
  • 答: 执行函数啊!
  • 问: 有没有对象上下文!?this指向谁!?
  • 答: 哈哈哈哈哈我懂了,this指向obj

这就是我们为什么需要注意函数表达式和函数声明的原因,因为在有些时候可能因为你使用函数表达式的方式对函数引用进行了传递,执行后从而得到你意想不到的效果。 注意跑!