js中函数和构造函数

236 阅读4分钟

平时遇到js的基础知识时总是会遗忘和混淆,所以整理了一下红宝书相关的知识点,方便自己快速复习查阅。(虽然是抄书但也有自己的整理思路啦)
本篇整理了关于函数和构造函数的一些容易被忽略和混淆的知识点,点击查看原文

函数

语法

ECMAScript 中的函数使用 function 关键字来声明,后跟一组参数以及函数体。函数的基本语法如下所示:

function functionName(arg0, arg1,...,argN) { 
 statements 
}

返回值

ECMAScript 中的函数在定义时不必指定是否返回值。 函数可以通过return 语句后跟要返回的值来实现返回值。return 语句也可以不带有任何返回值,此时函数将返回undefined。不写return语句,函数也会默认返回undefined。

function fn1(a, b) {
  return a + b;
}
function fn2() {}
function fn3() {
  return;
}
var a = fn1(1, 2); // 3
var b = fn2(); // undefined
var c = fn3(); // undefined

内部属性

在函数内部,有两个特殊的对象:arguments 和 this。

arguments

ECMAScript 中函数的参数在内部是用一个数组来表示的。在函数体内可以通过 arguments 对象来访问这个参数数组。

function sayHi(/* name, message */) {
  // alert('Hello ' + name + ',' + message);
  alert('Hello ' + arguments[0] + ',' + arguments[1]);
  console.log(arguments.length);
}
sayHi('ken', 'you are handsome.'); // 2

arguments 对象的长度是由传入的参数个数决定的,不是由定义函数时的命名参数的个数决定的。

function sayHi( name, message) {
  console.log(arguments.length);
}
sayHi('ken'); // 1
sayHi('ken', 'you are handsome.'); // 2

arguments.callee

arguments对象还有个属性叫callee,该属性是一个指针,指向拥有这个 arguments 对象的函数。请看下面这个非常经典的阶乘函数。

function factorial(num) {
  if (num <= 1) {
	return 1;
  } else {
	// 1、使用具名函数递归
	// return num * factorial(num - 1);
	// 2、使用arguments.callee
	return num * arguments.callee(num - 1);
  }
}

使用arguments.callee的好处是对函数执行和函数名factorial进行解耦。这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。

this

JavaScript 的 this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。但是ES6的箭头函数不同:箭头函数的函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。 除去不常用的 with 和 eval 的情况,具体到实际应用中,this 的指向大致可以分为以下 4 种。

  1. 作为对象的方法调用。当函数作为对象的方法被调用时,this 指向该对象;
  2. 作为普通函数调用,此时的 this 总是指向全局对象。在浏览器的 JavaScript 里,这个全局对象是 window 对象; (ECMAScript 5 的 strict 模式下this指向undefined)
window.name = 'globalName';
function globalGetName() {
  return this.name;
}
var myObject = {
  name: 'sven',
  getName: function () {
	return this.name;
  },
};
var getName = myObject.getName;
// 作为对象的方法调用
console.log(myObject.getName()); // sven
// 作为普通函数调用
console.log(globalGetName()); // globalName
console.log(getName()); // globalName
  1. 构造器调用。当用 new 运算符调用函数时,被调用的函数称作为构造函数。
var MyClass = function () {
  this.name = 'sven';
};
var obj = new MyClass();
alert(obj.name); // 输出:sven

⚠️但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this。详情请看下方的构造函数小节的讲解。

  1. Function.prototype.call 或 Function.prototype.apply 调用。 Function.prototype.call 和 Function.prototype.apply 可以动态地改变传入函数的 this
var obj1 = {
  name: 'sven',
  getName: function () {
	return this.name;
  },
};
var obj2 = {
  name: 'anne',
};
console.log(obj1.getName()); // 输出: sven
console.log(obj1.getName.call(obj2)); // 输出:anne

属性和方法

ECMAScript 中的函数是对象,因此函数也有属性和方法。

属性(length 、 prototype、caller)

length 属性表示函数希望接收的命名参数的个数。

function sayHi( name, message) {}
sayHi.length; // 2

prototype,我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象(即实例的原型对象,同时实例是通过调用构造函数创建的,他们的关系在原型章节进行总结)。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。prototype 属性是不可枚举的,因此使用 for-in 无法发现。

function Person(name, age, job) {
 // 在构造函数中定义实例信息
 this.name = name;
 this.age = age;
 this.job = job;
}
// 在原型对象添加信息
Person.prototype.sayName = function () {
 console.log(this.name);
};
var person1 = new Person('Nicholas', 29, 'Software Engineer');
person1.sayName(); // Nicholas
console.log(person1 instanceof Person); // true
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype === person1.__proto__); // true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

caller,caller属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为 null。

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
  // 使用arguments.callee将函数执行与函数名解耦
  console.log(arguments.callee.caller);
}
inner(); // null
outer(); // function outer() {inner();}

inner.caller或者arguments.callee.caller,指向了调用inner函数的函数,即outer函数

方法(apply 、call、bind 和 toLocaleString、toString、valueOf)

apply()和 call()

ECAMScript 3给Function的原型定义了两个方法,它们是Function.prototype.callFunction. prototype.apply。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。

apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是arguments 对象。
call()方法与apply()方法的作用相同,它们的区别仅在于接收参数的方式不同,传递给函数的参数必须逐个列举出来。

function sum(num1, num2) {
 return num1 + num2;
}
function applySum1(num1, num2) {
 return sum.apply(this, arguments); // 传入 arguments 对象
}
function applySum2(num1, num2) {
 return sum.apply(this, [num1, num2]); // 传入数组
}
function callSum(num1, num2){ 
 return sum.call(this, num1, num2); 
}
alert(applySum1(10,10)); //20
alert(applySum2(10, 10)); //20
alert(callSum(10, 10)); //20

apply()和 call()真正强大的地方是能够扩充函数赖以运行的作用域。使用 call() 或 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

bind()

ECMAScript 5 还定义了一个方法:bind()。这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind()函数的值。

window.color = 'red';
var o = { color: 'blue' };
function sayColor() {
 alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); //blue

函数绑定就是使用了函数的原生bind方法,他的优点请参考红宝书相关章节。
bind()函数接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。函数绑定主要用于事件处理程序以及 setTimeout() 和 setInterval()。然而,被绑定函数与普通函数相比有更多的开销,它们需要更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。 JavaScript 中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用 bind()还是 curry()要根据是否需要 object 对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。

toLocaleString、toString、valueOf

toLocaleString、toString、valueOf都是函数继承的方法,继承于object对象。函数调用这三个方法始终都返回函数的代码。返回代码的格式则因浏览器而异。

分清caller和callee

函数的属性caller:是保存着调用当前函数的函数的引用。如果是在全局作用域中调用当前函数,它的值为 null。严格模式下不能为函数的 caller 属性赋值,否则会导致错误。 arguments对象的属性caller:在严格模式下访问它也会导致错误,而在非严格模式下这个属性始终是undefined。

函数的属性callee: 无( 好像没有,我暂时没查到这个属性) arguments对象的属性callee:该属性是一个指针,指向拥有这个 arguments 对象的函数。当函数在严格模式下运行时,访问 arguments.callee 会导致错误。

构造函数

当用 new 运算符调用函数时,被调用的函数称作为构造函数。

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
	alert(this.name);
  };
}
var person1 = new Person('Nicholas', 29, 'Software Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

用 new 运算符调用函数时实际上会经历以下 4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

⚠️注意 如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,此时构造函数总会返回一个对象,构造函数里的 this 就指向返回的这个对象。 如果构造器显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this。

// 默认返回新创建的对象obj
var MyClass1 = function () {
  this.name = 'sven1';
};
var obj1 = new MyClass1(); // {name: "sven1"}
console.log(obj1.name); // 输出:sven
// 显式地返回一个对象1
var MyClass2 = function () {
  this.name = 'sven2';
  return {
	name: 'anne2',
  };
};
var obj2 = new MyClass2(); // {name: "anne2"}
console.log(obj2.name); // 输出:anne2
// 显式地返回一个对象2
var MyClass3 = function () {
  this.name = 'sven3';
  var newObj = {
	name: 'anne3',
  };
  return newObj;
};
var obj3 = new MyClass3(); // {name: "anne3"}
console.log(obj3.name); // 输出:anne3
// 返回非对象类型的数据
var MyClass4 = function () {
  this.name = 'sven4';
  return 'not a object';
};
var obj4 = new MyClass4(); // {name: "sven4"}
console.log(obj4.name); // 输出:sven4