JS面向对象函数的四种调用模式
函数的四种调用模式
概念
- 在 js 中,无论是函数, 还是方法, 还是事件, 还是构造器,...这些东西的本质都是函数
- 函数, 方法, 事件, 构造器,...只是所处的位置不同
- 这四种模式分别是
- 函数模式
- 方法模式
- 构造器模式
- 上下文模式
函数模式
特征: 简单的函数调用, 函数名前面没有任何引导内容
function foo(){}
var fn = function(){};
...
foo();
fn();
(function(){})();
// 上面的三种都是简单的函数调用
this的含义
- 在函数中 this 表示全局对象
- 在浏览器中 this 表示 window(js 中的 global)
方法模式
特征: 方法一定是依附于一个对象, 将函数赋值给对象的一个属性, 那么就成为方法.就是函数前面必须有引导对象
function foo(){
this.method = function(){};
}
var o = {
method : function(){}
}
this的含义
- 这个依附的对象(引导函数的对象)
注意点
在 arguments 这种伪数组, 或者 [] 数组这样的对象中, 这样调用函数也是方法调用, this 会只指向对象
构造器模式
构造函数在创建对象的时候, 做了些什么
- 使用 new 引导构造函数, 创建了一个实例对象
- 在创建对象的同时, 将this指向这个刚刚创建的对象
- 在构造函数中, 不需要 return , 会默认的 return this
分析:
由于构造函数只是给 this 添加成员, 而方法也可以完成这个操作,对与 this 来说, 构造函数和方法没有本质区别
关于return的补充, 在构造函数中
普通情况, 可以理解为构造函数已经默认进行了 return this, 添加在后面的都不会执行
- 如果手动的添加 return ,就相当于 return this.
- 如果手动的添加 return 基本类型(字符串, 数字, 布尔), 无效, 还是 return this
- 如果手动的添加 return null 或 return undefined, 无效, 还是 return this
特殊情况, return 对象, 最终返回对象
- 手动添加 return 对象类型, 那么原来创建的 this 会被丢掉, 返回 return 后面的对象
上下文模式
概念: 上下文就是环境, 就是自定义this的含义
语法:
- 函数名.apply( 对象, [参数]);
- 这个参数可以是数组, 也可以是伪数组
- 函数名.call( 对象, 参数);
- 多个参数可以通过
,
进行隔离
- 多个参数可以通过
描述:
- 函数名表示的是函数本身, 使用函数进行调用的时候,默认this指的是全局变量
- 函数名也可以是方法提供, 使用方法调用的时候, this指的是当前对象
- 使用 apply 或者 call 进行调用后, 无论是函数, 还是方法的 this 指向全部无效了, this 的指向由 apply 或者 call 的第一个参数决定
注意:
- 如果函数或方法中没有this的操作, 那么无论是哪一种函数调用模式, 其实都一样
- 如果是函数调用 foo(), 其实和 foo.apply(window) 类似
- 如果是方法调用 o.method(), 其实和 o.method.apply(o)
无论是 call 还是 apply 在没有后面参数的情况下(函数无参数, 方法无参数), 两者一致
function foo(){
console.log(this); // this => window
}
var obj = {};
foo.apply( obj ); // this => obj
foo.call( obj ); // this => obj
apply 和 call 第一个参数的使用规则
- 如果传入的是一个对象, 就相当于设置该函数中的this为参数
- 如果不传参数, 或者传入 null, undefined 等,那么this就默认是 window
foo();
foo.apply();
foo.apply(null);
foo.apply(undefined);
foo.call();
foo.call(null);
foo.call(undefined);
// 上面都this都指向window
- 如果传入的是基本类型, 那么this指向的就是基本类型的包装类型的引用
- number => Number
- boolean => Boolean
- string => String
除 this 外的其他参数
再使用上下文调用的时候, 原函数(方法)可能会带有参数, 那么要让这些参数在上下文中调用, 就需要这个第二, ( n )个参数来表示
function foo(num){
console.log(num);
}
foo.apply(null, [123]);
// 相当于
foo(123);
应用
上下文调用只是修改this, 但是使用最多的地方是借用函数调用
- 将伪数组转换为数组
- 传统方法
var a = {}; a[0] = 'a'; a[1] = 'b'; a.length = 2; // 使用数组自带方法 concat(); // 不修改原数组 var arr = []; var newArr = arr.concat(a);
分析
由于 a 是伪数组, 并不是真正的数组, 不能使用数组的方法, concat 会将 a 作为一个整体 Object 加入数组
apply 方法有一个特性, 可以将数组或者伪数组作为参数foo.apply( obj, 伪数组 ); // IE8 不支持
将 a 作为 apply 的第二个参数
var arr = []; var newArr = Array.prototype.concat.apply( arr, a);
由上面的数组转换, 我们可以得到结论, 应该涉及到数组操作的方法理论上都应该可以
push, pop, unshift, shift
slice
splice - 让伪数组使用 push 方法
小技巧, 伪数组添加元素
伪数组使用 push 方法var a = {length : 0}; // 设置伪数组的长度 a[ a.length++ ] = 'a'; a[ a.length++ ] = 'b'; // 在给伪数组的元素赋值时, 同时修改伪数组的 length 属性 // a[0] = 'a'; a.length++;
var arr = []; arr.push.apply( arr, a ); // 利用 apply 可以处理伪数组的特性, 进行传参 // 相当于 arr.push(a[0], a[1])
- 让伪数组使用 slice 方法
数组的 slice 语法- arr.slice( index, endIndex ), 不包含 endIndex
- 如果第二个参数不传参, 那么截取从 index 一直到数组结尾
- slice 方法不会修改原数组
通过 apply 实现伪数组使用 slice 方法
var a = { length : 0 }; a[a.length++] = 'a'; a[a.length++] = 'b'; var arr =[]; var newArr = arr.slice.apply(a ,[0])
获取数组中的最大值
传统方法
var arr = [1,2,3,4,5,6,7,8,9];
var max = arr[0];
for(var i = 0;i < arr.length;i++){
if(max < arr[i]){
max = arr[i]
}
}
使用 apply 借用 Math.max 获取数组中最大值
利用 apply 可以传入参数可以是数组或是伪数组的特性
var arr = [1,2,3,4,5,6,7,8,9];
Math.max.apply(null, arr);
创建对象的几种模式
了解了四种函数调用模式, 我们可以深一步了解创建对象的几种方式, 对象是通过构造函数创建的
- 工厂模式
特点:- 大量重复执行的代码, 解决重复实例化的问题
- 函数创建对象并返回
- 最典型的工厂模式就是
document.createElement()
- 无法知道是谁创建了这个实例对象
function createPerson(name, age, gender){ var o = {}; o.name = name; o.age = age; o.gender = gender; return o; }
- 构造方法
特点:- 解决了重复实例化问题
- 能够知道是谁创建了这个对象(constructor)
- 需要通过 new 运算符穿件对象
function Person(name,age,gender){ this.name = name; this.age = age; this.gender = gender; }
- 寄生式构造函数创建对象
特点:- 外表看起来就是构造犯法, 但本质不是通过构造方法创建对象
- 工厂模式 + 构造函数模式
- 不能确定对象的关系, 不推荐使用
function createPerson(name,age,gender){ var o = {}; o.name = name; o.age = age; o.gender = gender; return o; } var p = new createPerson('Bob',19,'male')
- 混合式创建
- 构造函数 + 原型
- 解决了构造函数传参和共享的问题
- 不共享的参数使用构造函数
- 共享的使用原型
- 这种混合模式很好的解决了传参和引用共享的难题
function createPerson(name,age,gender){ this.name = name; this.age = age; this.gender = gender; } createPerson.prototype = { constructor : createPerson, wife : '高圆圆' }
- 借用构造函数继承(对象冒充)
特点:- 借用构造函数(对象冒充)只能继承构造函数的成员, 无法继承原型的成员
function Person(name,age,gender){ this.name = name; this.age = age; this.gender = gender; } function Studner(name,age,gender,course){ // 借用构造函数Person, 创建 Student 对象 Person.call(this,name,age,gender); this.course = course; } var boy = new Student('Bob',19,'male',;Math);
- 寄生式继承
特点:- 原型 + 工厂模式
- 通过临时中转
// 临时中转 function person(o){ function Foo(){}; F.prototype = o; return new F(); } // 寄生函数 function create(o){ var fn = person(o); fn.move = function(){}; fn.eat = function(){}; fn.sleep = function(){}; return fn; } var boy = { name : 'Bob', age : 19, famliy : ['father','mother','wife'] } var boy1 = create(boy); console.log(boy1.name); console.log(boy1.family); // 此时 boy1 有了 boy 的成员
经典例题
例题 1
function Foo(){
getName = function(){ alert(1); };
return this;
}
function getName(){
alert(5);
}
Foo.getName = function(){ alert(2); };
Foo.prototype.getName = function(){ alert(3); };
getName = function(){ alert(4); };
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 4 1
getName(); // 4 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
分析
- 预解析
- 函数名 Foo 和函数名 getName 声明提升,函数名和函数体绑定
- 执行代码
- 执行 Foo.getName();
- 输出 2
- 执行 getName();
- 此时 getName 是在全局中, 未被修改, 输出4
- Foo().getName();
- 此事 Foo() 只是一个函数, 执行完成后 getName 被重新赋值
- getName 因为被重新赋值为 1, 输出1
- getName()
- 由于 getName 被重新赋值, 所以输出 1
- new Foo.getName();
- Foo.getName 并未被修改
- new 没有起任何作用
- 输出2
- new Foo().getName();
- 构造函数创建了实例对象
- 对象中没有 getName 方法, 要从对象的构造函数中的原型中寻找
- 在 Foo.prototype 中得到 getName 输出为 3
- new new Foo().getName();
- 构造函数创建了实例对象
- 对象中没有 getName 方法, 要从对象的构造函数中的原型中寻找
- new 没有起作用
- 在 Foo.prototype 中得到 getName 输出为 3
例题 2
var length = 10;
function fn() {
console.log( this.length );
}
var obj = {
length: 5,
method: function ( fn ) {
fn();
arguments[ 0 ](); // [ fn, 1, 2, 3, length: 4]
}
};
obj.method( fn, 1, 2, 3 );
分析
- 预解析
- 变量名 length, obj 和 函数名fn 声明提升, 函数名和函数体绑定
- 执行代码
- length = 10, 此时 length 可以看作是 window 下的属性
- obj = {}, 进行赋值
- 执行 obj 中的 method 方法
- 将 函数体 fn 进行传参
- 跳进函数 fn,执行函数 fn(), 函数中的 this 指的是 window 下的 length, 为10
- 明确argument是一个对象, argument[0] 指的是 fn
- 使用对象调用函数, 这里的 this 指的是 argument 这个对象
- 得到 argument.length 为 4
- 最后输出结果为 10 4
例题 3
var o = {
method : function(){
console.log(this);
}
};
o.method();
var f = o.method;
f();
(o.method)();
var obj = {};
(obj.fn = o.method)();
(obj.fn)();
分析
- 预解析
- 变量名 o, f ,obj 声明提升
- 执行函数
- o = {},进行赋值
- 执行o.method方法, 这里的 this 指的是 o 这个对象
- 将 o.method 函数体,赋值给f
- 执行 f(), 这里的 this 指的是window
- (o.method)是一个函数表达式, 和 o.method() 的结果一致
- obj = {}, obj进行赋值
- o.method 的函数体, 赋值给obj.fn, 执行之后, 这里的 this 指的是window
- (obj.fn)是一个函数表达式, 和 obj.fn() 的结果一致, tshi 指向 obj