JavaScript中函数的四种调用方式总结
作者:Knockkk
在《JavaScript忍者秘籍》书中提到,我们有四种不同的方式进行函数调用:
- 作为一个函数进行调用,这是最简单的形式。
- 作为一个方法进行调用,在对象上进行调用,支持面向对象编程。
- 作为构造器进行调用,创建一个新对象。
- 通过apply() 或call() 方法进行调用。
作为一个曾经的小镇做题家,一些关键词自然而然就冒出来了,this、闭包、原型......这些概念都不陌生,但总感觉有些散乱。我发现,顺着书中这几种函数调用方式,层层递进,倒是可以帮助更好的理解和串联这些知识点。
1. 作为函数调用
这里指的就是最简单直接的调用方式:
function sum(a, b) { return a + b; } sum(1, 2)
函数封装了一段逻辑,给一个输入,得到一个输出。函数最大的好处是它是可以复用的,而不是在每个需要的地方都写一段重复的代码。用好函数往往可以让代码变得更清晰。
使用函数时必然要关注它的作用域。简单来说,函数内是局部变量,只能在这个函数中访问,而全局变量可以在程序的任何代码中访问。但在JS中比这复杂,因为函数是可以嵌套的,也可以作为参数传递。比如下面的例子:
function outer() { let value = 0; function inner(n) { value += n; return value; } return inner; } const fn = outer(); fn(1); // 1 fn(2); // 3
此时inner()
作为内部函数是可以访问到外部函数outer()
内定义的局部变量的,即使它们是两个不同的函数。另外,正常来说,一个函数执行完成后,所有函数内定义的局部变量都会被回收,但这里不同:outer()
执行完成了,返回了一个函数,而这个函数内仍然能访问value
这个局部变量(value
并没有被回收)——即“闭包”。因为JS支持更灵活的函数使用,所以也就引出了这些概念,这在Go语言中也是一摸一样的。
2. 作为方法进行调用
即函数作为对象的一个属性,如下所示:
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello~"); }, }; dog.sayHi(); // Hello~
JS中可以用对象表示一些属性的集合,比如一只狗有名字和年龄。当然这个对象也可以有方法,比如这只狗会打招呼。如果想在打招呼的时候报出自己的名字怎么做呢?当然我们可以直接用对象名访问,即dog.name
,但还有一种更友好的方式——this
(在Java中,关键字“this”表示当前对象的引用,而在JS中,“this”是支持面向对象编码的主要手段之一)。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; dog.sayHi(); // Hello,my name is wangcai
这样就清晰多了,不用管变量名的dog是哪个dog,this就是本狗了。那普通函数不在对象中,它调用时的this是什么呢?
function getThis() { return this; } getThis() === window // true
在浏览器中,答案就是全局环境window。回到前面dog的例子,下面代码中this又是什么呢?是本狗吗?答案我们在后面揭晓。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi();
3. 作为构造器进行调用
如果我有很多条狗,那么用对象来一个个声明就略显笨拙了。你可能会写一个createDog
的函数,来按模式批量生产dog。
function createDog(name, age) { return { name, age, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; } const dog = createDog("wangcai", 1);
但同样,有一种更“面向对象”,更友好的方式——构造函数(constructor,ES6的“class”就是构造函数的语法糖) 。
function Dog(name, age) { this.name = name; this.age = age; this.sayHi = function () { console.log("Hello,my name is ", this.name); }; } const dog = new Dog("wangcai", 1);
构造函数需要搭配“new”关键字使用,它将自动返回一个新的对象。另外,构造函数一般用大写开头,同时它应该是一个名词,而不是动词。
构造函数只是一个约定,语言本身并没有限制你如何使用它。你可以直接执行Dog("wangcai",i)
,但这样没有什么意义,还会产生一些意外的全局变量。
我们可以结合前一节的“对象方法”来理解构造器的调用过程:
- 创建一个空对象
- 在该对象上执行这个函数(函数调用时的this指向这个对象)
- 最后将这个对象返回,如果没有显式的返回值(还差了一个步骤,暂时没涉及,后面再说)
JS中几乎所有对象都有自己的构造函数,对于使用字面量语法声明的对象,它的构造函数就是Object。
const dog = { name: "wangcai" };
JS中有三种方式创建一个对象。
- 字面量语法,如
const obj = { value: 100 }
Object.create()
- 使用new调用构造函数初始化一个对象
只有通过Object.create(null)
创建的对象是没有构造函数的,也没有“原型”。
对象与构造函数之间通过原型进行关联。还是以我们的dog为例:
const dog = new Dog("wangcai", 1); Object.getPrototypeOf(dog) === Dog.prototype // true
“当构造函数搭配new
使用时,该函数的prototype数据属性将用作新对象的原型。默认情况下,函数的prototype是一个普通的对象。这个对象具有一个属性:constructor,它是对这个函数本身的一个引用。 constructor 属性是可编辑、可配置但不可枚举的”。所以,我们通过Object.getPrototypeOf(dog).constructor
可以直接获取到对象的构造函数。下面就是浏览器控制台打印出的dog对象:
那对象的原型又是什么呢?它是JS中一种独特的机制,它的特点如下:
- 每个对象都有一个私有属性指向另一个名为原型(prototype)的对象。当访问一个对象属性时,如果属性不存在,就会继续查找这个对象的原型属性。
- 原型对象也有一个自己的原型,层层向上直到一个对象的原型为null。
Object.prototype
的原型始终为null且不可更改。
所以,一个对象不仅有实例属性,还有原型属性,它们都可以被访问到,只是原型属性是不可枚举的。
const o = { value: 100}; Object.keys(o); // ['value'] o.valueOf(); // valueOf是构造函数Object的prototype上定义的方法,可以正常访问
再回顾new
进行初始化的过程,是缺了什么步骤呢?就是将对象的原型指向构造函数的prototype。我们可以按这个规则模拟一个new函数。
function Dog(name, age) { this.name = name; this.age = age; } Dog.prototype.sayHi = function () { console.log("Hello,my name is ", this.name); }; function myNew(fn, ...args) { const obj = Object.create(fn.prototype); // 创建一个对象,将对象的原型指向fn.prototype const res = fn.apply(obj, args); return res ?? obj; } const dog = myNew(Dog, "wangcai", 1); dog.sayHi(); // Hello,my name is wangcai Object.getPrototypeOf(dog) === Dog.prototype; // true
大家细心会发现,这个例子中将sayHi
函数放在了Dog的prototype对象上,而不像之前在函数内通过this.sayHi
声明。两种方式new出来的对象都是能调用sayHi()
的,唯一的区别是:一个是通过对象的实例属性访问,而另一个是通过原型属性访问。后者看起来是这样的:
原型属性具有一些优点,比如在这个例子中,每个new出来的对象访问的都是同一个sayHi
函数(定义在Dog的prototype对象上),而不是重新拷贝,这样节省内存。同时,sayHi
函数只需修改一次,就能应用到所有实例化的对象中。
Dog.prototype.sayHi = function () { console.log("Good morning~"); }; dog.sayHi(); // Good morning~
原型链的特性还能支持我们实现对象的“继承”。首先我们用class语法试下继承的效果。
// 基类 class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} is eating`); } } //派生类 class Dog extends Animal { constructor(name, age) { super(name); this.age = age; } sayHi() { console.log(`Hello, my name is ${this.name}, I am ${this.age} years old`); } } const dog = new Dog("wangcai", 1); dog.sayHi(); // Hello, my name is wangcai, I am 1 years old dog.eat(); // wangcai is eating
分析一下,age
与name
都在dog对象的实例属性上,而sayHi()
函数在dog对象的原型上,即Dog.prototype
。再往上一层,Dog.prototype
这个对象的原型是Animal.prototype
,Animal.prototype
上具有eat()
方法。那再往上一层呢?Animal.prototype这个对象的原型是Object,再往上就到原型链顶端null了。
接下来相信大家也有概念怎么手动实现继承了。下面是一个示例:
function Animal(name) { this.name = name; } Animal.prototype.eat = function () { console.log(`${this.name} is eating`); }; function Dog(name, age) { this.age = age; Animal.call(this, name); // 将Animal的实例属性放到Dog对象上 Object.setPrototypeOf(Dog.prototype, Animal.prototype); // 设置原型链 } Dog.prototype.sayHi = function () { console.log(`Hello, my name is ${this.name}, I am ${this.age} years old`); }; const dog = new Dog("wangcai", 1); dog.sayHi(); dog.eat();
4. 通过apply()或call()方法进行调用
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi();
再回顾前面的例子,揭晓答案,结果是Hello,my name is undifined
,显然不是本狗了。
为什么会这样呢?因为JS中函数的“this”是由调用点决定的(在运行时确定) ,而不是在函数声明处决定。这就让人很困扰了,本狗的名字都对不上了,这样真的好吗?其实这样是为了提供更大的灵活性——支持动态上下文。
function sayHi() { console.log("Hello,my name is ", this.name); } const dog = { name: "wangcai", sayHi, }; const cat = { name: "miaomiao", sayHi, }; dog.sayHi(); // Hello,my name is wangcai cat.sayHi(); // Hello,my name is miaomiao
通过允许this
由调用点动态确定,可以让同一个函数在不同的对象上使用。另外,JS也提供了显式指定函数执行时的this为某个对象的方法,即apply()
或call()
。
const dog = { name: "wangcai", age: 1, sayHi: function () { console.log("Hello,my name is ", this.name); }, }; const sayHi = dog.sayHi; sayHi.apply(dog); // Hello,my name is wangcai sayHi.call(dog); // Hello,my name is wangcai
很开心,本狗又回来了。apply()
与call()
只有函数传参上的差异,在使用时看哪个方便用哪个就行。
总结
通过体验函数这四种不同的调用方式,我们逐渐接触了JS中一些底层的知识点:作为函数调用时的闭包,作为方法调用时的this指向,作为构造器调用时的原型。JS中的函数非常灵活,也很强大,并且与对象有着密切的联系。
以上就是JavaScript中函数的四种调用方式总结的详细内容,更多关于JavaScript函数调用的资料请关注脚本之家其它相关文章!