学习日记

原文地址: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 // 2
length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(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内部声明了一个函数barbar的作用域绑定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很像数组,但它是一个对象。数组专有的方法(比如sliceforEach),不能在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

posted @ 2020-07-18 15:21  miniFour  阅读(178)  评论(0)    收藏  举报