JavaScript 精粹 基础 进阶函数和作用域(函数this)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 精粹 基础 进阶函数和作用域(函数this)相关的知识,希望对你有一定的参考价值。

转载请注明出处

原文连接 http://blog.huanghanlian.com/article/5b698ef6b8ea642ea9213f4e

函数是一块javascript代码,被定义一次,但可执行调用多次,js中的函数也是对象,所以js函数可以像其他对象那样操作和传递所以我们也常叫js中的函数为函数对象。

函数概述

函数的构成主要有几个部分函数名,参数列表,函数体

技术图片

function foo(x, y) {
    if (typeof x === ‘number‘ && typeof y === ‘number‘) {
        return x + y;
    } else {
        return 0;
    }
}
var cdr = foo(1,2);
console.log(cdr);//3

需要注意的是函数的返回值是依赖与return语句的,如果没有return语句,默认会在所有代码执行以后返回一个undefined。那么这是一般的函数调用。

function foo(x, y) {
    if (typeof x === ‘number‘ && typeof y === ‘number‘) {} else {

    }
}
var cdr = foo(1, 2);
console.log(cdr);//undefined

如果是作为构造器,外部使用new去调用的话,如果没有return语句,或者return是基本类型的话,那么会将this作为返回

function foo(x, y) {
    if (typeof x === ‘number‘ && typeof y === ‘number‘) {} else {
        return x+y;
    }
    this.x=x+y;
}
var cdr = new foo(1, 2);
console.log(cdr.x);//3

重点

this

函数在不同的调用方式下他的this指向是不一样的,并且不同的调用方式下也会有一些细微的差别。

arguments

函数里面有一个特殊的对象,叫arguments他和参数是有一定的连带关系的。

function foo(a,b) {
    return arguments;
}
var cdr=foo(1,2)
console.log(cdr);

技术图片

不同的调用方式

直接调用 对象方法 构造器 call/apply/bind
foo() o.method() new Foo() func.call(o);

函数声明与表达式

创建函数有不同的方式,常见的两种就是函数声明函数表达式

函数声明

function foo(a, b) {
    a=+a;
    b=+b;
    if (isNaN(a)||isNaN(b)) {   //isNaN() 函数用于检查其参数是否是非数字值。是数字返回true,非数字返回false
        return;
    }
    return a+b;
}
var cdr = foo(1, 2);
console.log(cdr);//3

函数表达式

把一个匿名函数赋值给一个变量,这种就是函数表达式

var add = function(a, b) {
    //do sth
}

需要注意的是函数声明的函数可以在函数前调用

add();
function add() {
    console.log(1);//1
}

但是函数表达式会报错Uncaught TypeError: add is not a function

add();
var add = function() {
    alert(1)
}

函数表达式-立即执行函数表达式

把一个匿名函数用一个括号()括起来,然后再去直接调用,这种函数定义的方式呢,也叫作函数表达式,并且是立即执行函数表达式。

(function() {
    console.log(1); //1
})();

return 函数

我们也可以将函数对象,作为一个返回值,因为函数也是对象。这种形式也是函数表达式。

return function(){
    //do sth;
};

命名式函数表达式

这种并不常见,同样赋值给给一个函数,但是这个函数不是匿名函数,而是有一个名字的函数。这就是命名式函数表达式

var add = function add() {
    //do sth
};

除了函数声明和函数表达式创建函数以外,还有一种不常见的一种创建函数对象的方式。就是函数构造器Function构造器

Function构造器

函数构造器就是大写的Function

var add = new Function (‘a‘,‘b‘,‘console.log(a+b);‘);
add(1,2);//3

var add = Function (‘a‘,‘b‘,‘console.log(a+b);‘);
add(1,2);//3

可以通过new或者直接调用的方式 运行,他们俩基本没什么区别,

前面两个参数是函数对象里面的行参,最后一个参数表示函数体里面的代码。

用参数是字符串会带来安全上的一些隐患。

Function构造器去创建的函数里面,创建的变量仍然是局部变量,也可以使用立即调用。

Function (‘var lop="local";console.log(lop);‘)();//Function构造器去创建的函数并且立即调用
console.log(lop);//lop是局部变量外部无法访问会报错

在立即调用函数中有lo变量,他的内部有一个df函数声明函数这个函数内可以拿到lo变量

(function() {
    var lo = "lo";

    function df() {
        console.log(lo)
    }
    df();
})();
//输出lo

但是在Function构造器中lo却访问不到。
在立即调用函数里,lo不可访问,全局变量l可以访问。所以说Function构造器很少使用

var l="l";
(function(){
    var lo="lo";
    Function (‘console.log(l);console.log(lo);‘)();
})()

//l
//Uncaught ReferenceError: lo is not defined(…)

比一比三种创建函数的方式

函数声明 函数表达式 函数构造器
前置
允许匿名
立即调用
在定义该函数的作用域通过函数名访问
没有函数名
  • 前置:只有函数声明会被前置,就好像被拉倒了最前面,说以说可以在函数声明的位置之前,去调用这样一个函数。
    但是函数表达式和函数构造器是代码执行阶段,才会去创建对应的函数对象,所以不会被前置。

  • 允许匿名:函数表达式和函数构造器都是允许匿名的,事实上函数构造器只能是匿名的是不可以有名字的,但是函数声明他的名字是不可以省略的,省略在比较新的浏览器会报错。

  • 立即调用:函数声明是不能被立即调用的,如果你尝试把函数声明后面写一个括号,那么会报一个异常,因为函数声明被提前解析,并且前置放到最前了,所以会报错。函数表达式和函数构造器因为不会前置,所以表达式计算结果是一个函数对象的时候我们可以加一对括号把他立即去调用。

  • 在定义该函数的作用域通过函数名访问:比如说fuction fd(){}在function同级作用域下,可以通过fd()去调用这个函数,但是函数表达式和函数构造器就不可以这样。

  • 没有函数名:事实上函数构造器只能是匿名的是不可以有名字的

[JavaScript]this

在不同的环境下,同一个函数在不同的调用方式下,这个 this都有可能是不同的。

全局作用域下的this

全局作用域的 this一般指向全局对象,那么在浏览器里面这个全局对象就是window

console.log(this.document === document); //true
console.log(this === window); //true

可以看到this.document等价于window.document
this等价于window

this.a = 37;
console.log(window.a); //37

给全局对象添加一个属性a,这样就相当于创建了一个全局变量a,并且赋值为37。

一般函数的this(浏览器)

function f1() {
    return this;
}
console.log(f1() === window); //true

var f1 = function() {
    return this;
}
console.log(f1() === window); //true

用函数声明或者函数表达式,返回this,这里的this指向全局对象。

var f2 = function() {
    "use strict";
    return this;
}
console.log(f2()); //undefined

需要注意的是再严格模式下那么默认一般情况下this会指向

作为对象方法的函数的this

var o = {
    prop: 37,
    f: function() {
        return this.prop;
    }
};
console.log(o.f()); //37

创建了一个对象字面量oo里面有一个属性f值呢是一个函数对象,对于一个把函数作为对象属性值的方式,叫做对象的方法,作为对象的方法去调用o.f()这种情况下this会指向对象o所以o.f()被调用返回37。

var o = {
    prop: 37
};

function indep() {
    return this.prop;
}
o.f = indep;
console.log(o.f());//37

定义对象o只有一个属性prop赋值为37
这里买你有一个独立的indep函数,函数体return一个this.prop;如果直接去调用indep()那么这个this会指向window

如果用赋值方式o.fo对象创建属性f值为indep函数对象,那么这样去调用我们任然能够拿到37,console.log(o.f());//37

对象原型链上的this

Object.create({x:1});是系统内置的函数,这个函数会接收一个参数,一般是一个对象。他会返回一个新创建的对象,并且让这个对象的原型指向参数,参数一般是个对象。

var o = {
    f: function() {
        return this.a + this.b;
    }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p);

技术图片

原型链上有f方法

var o = {
    f: function() {
        return this.a + this.b;
    }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f());//5

创建o对象属性f值是一个函数对象,

通过var p = Object.create(o);创建了一个对象p,这个对象p是一个空的对象,并且他的原型会指向o,使用p.a给对象p添加属性值为1p.b值为4,这样就创建在对象上的属性,那么我去调用p原型上的方法的时候,this.a + this.b;任然能取到对象上的a和b。

get/set方法与this

function modulus() {
    return Math.sqrt(this.re * this.re + this.im * this.im);
};

var o = {
    re: 1,
    im: -1,
    get phase() {
        return Math.atan2(this.im, this.re);
    }
};

Object.defineProperty(o, ‘modulus‘, {
    get: modulus,
    enumerable: true,
    configurable: true
});

console.log(o.phase, o.modulus);//-0.7853981633974483 1.4142135623730951

构造器中的this

function MyClass() {
    this.a = 23;
};
var o = new MyClass();
console.log(o.a); //23

如果正常去调用一个函数的话MyClass()那么这个函数的this会指向window但是如果我用new实例化构造函数去调用,那么这里面的this会指向这样一个空的对象,并且这个对象的原型会指向MyClass.prototype
最后这个this.a会作为返回值,因为这里没有return所以默认this会作为返回值,所以这个对象o就会就会有a属性值23。

function c2() {
    this.a = 23;
    return {
        a: 24
    };
}
var f = new c2();
console.log(f.a); //24

c2函数this等于23,但是这一次return返回了一个对象,那么这个a就不再是23了,而是返回的这个对象,o.a属性就变成了24。

如果是作为构造器,外部使用new去调用的话,如果没有return语句,或者return是基本类型的话,那么会将this作为返回

call/apply方法与this

function add(c, d) {
    return this.a + this.b + c + d;
};

var o = {
    a: 1,
    b: 3
};

var h=add.call(o, 5, 7);//1+3+5+7
console.log(h)

add.apply(o, [10, 20]);//1+3+10+20

function bar() {
    console.log(Object.prototype.toString.call(this));
};

bar.call(7);//[object Number]

函数add,这里面返回this.a + this.b + c + d;把这四个数相加起来,定义o对象有两个属性a和b,通过对象call方法,.call(o, 5, 7)第一个参数是你想作为this的这样一个对象,5和7是想要添加的参数,5赋值给function add(c, d)的c,7赋值给d;最终的结果就是1+3+5+7
add.call(o, 5, 7)add.apply(o, [10, 20])基本没什么差别,只是apply是数组作为传参的。

bind方法与this

function f() {
    return this.a;
};

var g = f.bind({
    a: "test"
});
console.log(g()); //test

var o = {
    a: 23,
    f: f,
    g: g
};
console.log(o.f(), o.g()); //23 "test"

在EcmaScript5中扩展了叫bind的方法(IE6,7,8不支持)

这里有个函数对象f,通过bind方法,发现f.bind({ a: "test"});有一个参数,这个参数是一个对象,这个对象就是你想要将某一个对象作为this的时候那就把这样一个对象穿进去,那么我们拿到一个新的g对象,新的g对象在调用的时候this已经指向了bind的参数,这对于我们需要去绑定一次重复调用仍然实现绑定,这样会比call,apply会更加高效点。

还可以看到,这里有一个o然后把a赋值为23,f属性赋值为f函数,g赋值绑定之后的方法,输出f的时候能拿到23,一般的函数根据调用方式来判断,他是通过对象属性去调用的,那么这里买你的this就会指向o那么就会拿到a:23。这里面比较特殊的我使用bind的方法去绑定了之后,即使我们把 新绑定之后的方法作为对象的属性去调用,仍然会按照之前的绑定去走。所以返回"test"

[JavaScript]函数属性arguments

函数属性 & arguments

接触函数属性和方法

function foo(a, b, d) {
    console.log(arguments);
    console.log(arguments.length); //2
    console.log(arguments[0]); //1
    arguments[1] = 10;
    console.log(b); //change to 10;

    arguments[2] = 100;
    console.log(d); //即使手动赋值任然是undefined. 没有赋值arguments[2]=100;d等于undefined
    console.log(arguments.callee === foo);
}
foo(1, 2);
console.log(foo.length);
console.log(foo.name);
console.dir(foo);

技术图片

定义函数foo
通过foo.name获取函数名
通过foo.length获取函数行参个数
通过arguments.length获取函数实际传参的个数

有一个问题。foo(1, 2);传参只有两个参数,实际上d是没有传进来的, 这种情况尝试去arguments[2] = 100;去赋值的时候对应的d仍然是undefined

严格模式下

function foo(a, b, d) {
    ‘use strict‘
    console.log(arguments.length); //2
    console.log(arguments[0]); //1
    arguments[1] = 10;
    console.log(b); //严格模式下仍然是1

    arguments[2] = 100;
    console.log(d); //即使手动赋值任然是undefined. 没有赋值arguments[2]=100;d等于undefined
    console.log(arguments.callee === foo);  //严格模式下arguments.callee是禁止使用
}
foo(1, 2);
console.log(foo.length);
console.log(foo.name);

apply/call方法

function foo(x, y) {
    console.log(x, y, this);
};
foo.call(100, 1, 2);                    //1 2 Number {[[PrimitiveValue]]: 100}
foo.apply(true, [3, 4]);                //3 4 Boolean {[[PrimitiveValue]]: true}
foo.apply(null);                        //undefined undefined Window {...}
foo.apply(undefined);                   //undefined undefined Window {...}

定义foo函数,有x,y两个属性,输出参数和对应的this

对于call()来讲第一个参数就是想作为this的对象如果不是对象他会转成对象,所以这里的foo.call(100, 1, 2);100会转成对应的包装类Number值为100。类似的foo.apply(true, [3, 4]);true会转换成布尔值。
如果第一个参数是null,undefined的话,那么this会指向全局对象,对于浏览器就是window对象

需要注意在严格模式下

function foo(x, y) {
    ‘use strict‘;
    console.log(x, y, this);
};
foo.call(100, 1, 2);                    //1 2 100
foo.apply(true, [3, 4]);                //3 4 true
foo.apply(null);                        //undefined undefined null
foo.apply(undefined);                   //undefined undefined undefined

在严格模式下this输出直接量

bind方法

在EcmaScript5中扩展了叫bind的方法(IE6,7,8不支持)

this.x = 9; //创建了一个全局变量x并赋值为9
var mod = {
    x: 81,
    getX: function() {
        return this.x;
    }
};

console.log(mod.getX()); //81

var gitX = mod.getX;
console.log(gitX());; //9

var bound = gitX.bind(mod);
console.log(bound());//81

创建了一个全局变量x并赋值为9,然后有一个变量mod赋值为一个对象字面量,里面有x属性 值为81,有一个getX属性,值是一个匿名函数,他会返回this.x

如果对象(点)属性名 mod.getX()这样的调用方式。会返回这个对象为this返回81。

var gitX = mod.getX;如果定义一个全局变量gitX赋值为mod对象的getX属性,这个时候this就会指向全局对象,也就是this.x = 9;所以返回9,console.log(gitX());; //9

通过bind方法改变函数运行时的this指向,var bound = gitX.bind(mod);也就是说gitX函数在运行时thismod对象。所以再次输出81。

bind与currying 科里化功能

函数科里化就是把一个函数拆成多个单元。

function add(a,b,c){
    return a+b+c;
};

var func=add.bind(undefined,100);
func(1,2);//103

var func2=func.bind(undefined,200);
func2(10);//310

这里有一个函数add他的作用是把参数abc相加然后作为返回值,

可能有的时候不需要一次把函数都调用完,而是我调用一次把前面两个参数传完了后,得到这样的函数我再去调用,传入第三个值,比如通过add.bind()方法这次不需要改变this,那写个undefined,提高额外参数100,这个我们拿到bind函数以后,相当于这个100就会固定赋值给a第一个参数,在调用func(1,2);传入1和2的时候,1就会给b,2就会给c,所以最后的答案就是103。

类似的再去func.bind()也就是说绑定了两个参数ab,b传入200,再去调用一次func2(10)传入10,那么就是100+200+10,最终结果就是310。

实际例子

技术图片

bind与new

function foo() {
    this.b = 100;
    return this.a;
};

var func = foo.bind({
    a: 1
});
console.log(func()); //1

var func2 = new func();
console.log(func2); //{b: 100}

foo函数声明,函数体this.b=100return this.a;那如果我直接调用的话,this.b=100中的this是指向全局对象,所以说相当于创建了一个全部变量b并赋值为100,返回全局对象上的a属性。

使用bind()方法,var func = foo.bind({ a: 1 });来传入一个参数,一个字面量对象只有一个属性a,直接调用的话func(),那么foo函数中的this就会指向bind({a:1})的参数。所以调用func()返回1。

如果使用new的话就特殊些,使用new除非func()函数return 除非是对象如果不是对象将会把this作为返回值,并且this会被初始化成一个默认的空对象,这个空对象的原型是foo.prototype,所以使用new去调用的时候var func2 = new func();,即使用了var func = foo.bind({ a: 1 });bind方法但是这个this任然会指向没有bind的时候的一样,所以返回值会忽略return,返回this对象。

以上是关于JavaScript 精粹 基础 进阶函数和作用域(函数this)的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 精粹 基础 进阶OOP面向对象编程(下)

JavaScript语言精粹4递归(汗诺塔游戏寻常解)及作用域

JavaScript 精粹 基础 进阶对象

JavaScript 精粹 基础 进阶表达式和运算符

JavaScript 精粹 基础 进阶数据类型

JavaScript 精粹 基础 进阶语句