学习日记
原文地址:https://wangdoc.com/javascript/oop/this.html
函数
函数声明的三种方式:
(1)function命令 (2) 函数表达式 (3)Function 构造函数
采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function
上面代码在函数表达式中,加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)
// * Function 构造函数,总的来说这种声明函数的方式非常不直观,几乎无人使用 *
var add = new Function( 'x', 'y', 'return x + y' ); // 等同于 function add(x, y) { return x + y; }
函数可以调用自身,这就是递归。
函数名的提升:
javascript引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以下面的代码不会抱错
f() function f(){}
表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。
f() var f = function (){}; // TypeError: undefined is not a function //等同于 var f; f(); f = function (){}
//调用f的时候,f只是被声明了,还没有赋值,等于undefined,所以会抱错
注意,如果像下面例子那样,采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。
var f = function () { console.log('1'); } function f() { console.log('2'); } f() // 1
name属性返回函数的名字。
function f1(){ } f1.name //"f1"
如果是通过变量赋值定义的函数,那么name属性返回变量名。
var f2 = function (){}
f2.name // "f2"
如果变量的值是一个具名函数,返回function关键字之后的函数名。
注意,真正的函数名还是f3,没有Name这个名字只在函数体内部可用。
var f3 = function myName() {};
f3.name // 'myName'
// length属性,返回函数预期传入的参数个属。就是定义时候的参数个数,不是
调用时输入了多少个参数。
function f(a,b){}
f.length // 2length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。
函数的toString()方法返回一个字符串,内容是函数的源码。
对于那些原生的函数,toString()方法返回function (){[native code]}
Math.sqrt.toString() // "function sqrt() { [native code] }"
Math.sqrt()是js引擎提供的原生函数,toString()方法就返回原生代码的提示。
函数内部的注释也可以返回。
注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
if(true){ var x = 5 } console.error(x) //5
函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生”变量提升“现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数的头部。
function foo(x) { if (x > 100) { var tmp = x - 100; } } // 等同于 function foo(x) { var tmp; if (x > 100) { tmp = x - 100; }; }
函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
var a = 1; var x = function () { console.log(a); }; function f() { var a = 2; x(); } f() // 1
其中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2.
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
var x = function () { console.log(a); }; function y(f) { var a = 2; f(); } y(x) // ReferenceError: a is not defined
上面代码将函数x作为参数,传入函数y。但是,函数x是在函数y体外声明的,作用域绑定外层,因此找不到函数y的内部变量a,导致报错。
同样的,函数体内部声明的函数,作用域绑定函数体内部。
function foo() { var x = 1; function bar() { console.log(x); } return bar; } var x = 2; var f = foo(); f() // 1
上面代码中,函数foo内部声明了一个函数bar,bar的作用域绑定foo。当我们在foo外部取出bar执行时,变量x指向的是foo内部的x,而不是foo外部的x。正是这种机制,构成了下文要讲解的“闭包”现象。
//没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined。 function f(a,b){ return a } f( , 1) // SyntaxError: Unexpected token ,(…) f(undefined, 1) // undefined
var p = 2; function f(p) { p = 3; } f(p); p // 2
上面代码中,变量p是一个原始类型的值,传入函数f的方式是传值传递。因此,在函数内部,p的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
var obj = { p: 1 }; function f(o) { o.p = 2; } f(obj); obj.p // 2
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1,2,3]; function f(o){ o=[2,3,4]; } f(obj); obj // [1,2,3]
上面代码中,在函数f()内部,参数对象obj被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(o)的值实际是参数obj的地址,重新对o赋值导致o指向另一个地址,保存在原地址上的值当然不受影响。
同名参数如果有同名的参数,则取最后出现的那个值。
function f(a,a){ console.log(a)
console.error(arguments[0]) } f(1,2) //2,1 f(1) //undefined,1
//这是如果要获得第一个a的直,可以使用arguments对象。
var f = function (one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3 //正常模式下,arguments对象可以在运行时修改。 var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5 //严格模式下,arguments对象与函数参数不具有联动关系。也就是说,修改arguments对象不会影响到实际的函数参数。 var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 2
需要注意的是,虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。
如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments); // 或者 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
arguments对象有一个callee属性,返回它所对应的原函数。
var f = function (){ console.log(arguments.callee === f) } f() //true
//这个属性在严格模式下禁用的
面向对象编程
1、this
简单地说,this就是属性或方法“当前”所在的对象
this.property
上面代码中,this就代表property属性当前所在的对象。
var person = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:张三"
this指向person,this.name就是persion.name
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script>
上面代码是一个文本输入框,每个用户输入一个值,就会调用onchange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值。
总结一下,javascript语言中,一切皆对象,运行环境也是对象,所以函数都在某个对象之中运行,this就是函数运行时所在的对象(环境)。
javascript语言之所以有this的设计,跟内存里面的数据结构有关系。
var obj = { foo : 5 }
上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址。后面如果读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。
原始的对象以字典结构保存,每个属性名都对应个属性描述对象。例如,上面例子的foo属性,实际上是以下面的形式保存的。
{ foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } }
注意:foo属性的值保存在属性描述对象的value属性里面。
var obj = {foo:function(){}}
这时,引擎会将函数单独保存在内存中,然后将函数的地址赋值给foo属性的value属性。
{
foo:{
[[value]]:函数的地址
...
}
}
由于函数是一个单独的值,所以她可以在不同的环境(上下文)执行。
var f = function () {}; var obj = { f: f }; // 单独执行 f() // obj 环境执行 obj.f()
由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
this使用场合:
(1) 全局环境使用this,它指的就是顶层对象window。
(2) 构造函数中使用this,指的就是实例对象。
var Obj = function (p) { this.p = p; } // 定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个P属性。 var o = new Ojb('hello world'); o.p // 'hello world'
(3) 对象的方法。如果对象的方法里面包含this,this指向就是方法运行时所在的对象,该方法赋值给另一个对象,就会改变this的指向。
闭包
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999
闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function createIncrementor(start) { return function () { return start++; }; } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7
function(){ /* code */ }(); // SyntaxError: Unexpected token (
JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句。因此,JavaScript 引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }()); // 或者 (function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE
上面代码的两行之间如果没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); !function () { /* code */ }(); ~function () { /* code */ }(); -function () { /* code */ }(); +function () { /* code */ }();
eval() JSON.parse
-------------7月15日
waiting...
-------------7月16日
length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE
浙公网安备 33010602011771号