在js中,call和apply是二个神奇的方法,但同时也是容易令人迷惑的二个方法,call和apply的功能是以不同的对象作为上下文来调用某个函数的,简而言之,就是允许一个对象去调用另一个对象的成员函数,咋一看似乎很不可思议,而且还容易引起混乱,但其实js并没有严格的所谓’成员函数‘的概念,函数与对象的所属关系在调用时才展现出来,灵活使用call和apply可以节省不少时间,在后面我们可以看到,call可以用于实现对象的继承
call和apply的功能是一致的,二者细微的差别在于call以参数表来接受被调用函数的参数,而apply以数组来接受被调用函数的参数,call和apply的语法分别是:
func.call(thisArg[, arg1[, arg2[, ... ]]])
func.apply(thisArg[, argsArray] )
其中,func是函数的引用,thisArg是func被调用时的山修改文对象,arg1,arg2或argsArray是传入func的参数,我们以下面一段代码为例介绍call的工作机制:
1 var someuser = { 2 name: 'by', 3 display: function (words) { 4 console.log(this.name + ' says ' + words); 5 } 6 }; 7 var foo = { 8 name: 'foo' 9 }; 10 someuser.display.call(foo, 'hello'); //输出foo says hello
someuser.display是被调用的函数,它通过call将上下文改变为foo对象,因此在函数体内访问的this.name时实际上访问的就是foo.name,因此输出的即是foo.
2bind
如何改变被调用函数的上下文呢?前面说过,可以用call或apply方法,但如果重复使用不方便,因此每次都要把上下文对象作为参数传递,而且还会使代码变得不直观,针对这种情况,我们可以使用bind方法来永久的绑定函数的上下文,使其无论被谁调用,上下文都是固定的,bind语法如下:
func.bind(thisArg[, arg1[, arg2[, ...]]])
其中func是待绑定函数,thisArg是改变的上下文对象,arg1,arg2是绑定的参数表,bind方法返回值是上下文为thisArg的func,通过下面这个例子可以帮我们理解bind的使用方法:
1 var someuser = { 2 name: 'by', 3 func: function () { 4 console.log(this.name); 5 } 6 }; 7 var foo = { 8 name: 'foo' 9 }; 10 foo.func = someuser.func; 11 foo.func(); //输出foo 12 13 foo.func1 = someuser.func.bind(someuser); 14 foo.func1(); //输出by 15 16 func = someuser.func.bind(foo); 17 func(); //输出foo 18 19 func2 = func; 20 func2(); //输出foo
上面代码直接将foo.func 赋值给someuser.func,调用foo.func()时, this指针为foo,所以输出结果是foo.
foo.func1使用了bind方法,将someuser作为this指针绑定到someuser.func,调用foo.func1()时,this指针为someuser,所以输出结果是by.
全局函数func同样使用了bind方法,将foo作为this指针绑定到someuser.func,调用func()时,this指针为foo,所以输出结果是foo,而func2直接将绑定过的func赋值过来,与func行为完全相同
3使用 bind绑定参数表
bind方法还有一个重要功能,绑定参数表,如下例所示:
1 var person = { 2 name: 'by', 3 says: function (act, obj) { 4 console.log(this.name + ' ' + act + ' ' + obj); 5 } 6 }; 7 person.says('loves', 'diovyb'); //输出by loves diovyb 8 9 bya = person.says.bind(person, 'loves'); 10 bya('you'); //输出by loves you
可以看到,bya将this指针绑定到了person,并将第一个参数绑定到loves,之后在调用bya的时候,只需传入第三个参数,这个特性可以用于创建一个函数的’捷径‘,之后我们可以通过这个捷径调用,以便在代码多处调用时省略重复输入相同的参数
4理解bind
尽管bind很优美,还是有一些令人疑惑的地方,例如下面的代码:
1 var someuser = { 2 name: 'by', 3 func: function () { 4 console.log(this.name); 5 } 6 }; 7 var foo = { 8 name : 'foo' 9 }; 10 func = someuser.func.bind(foo); 11 func(); //输出foo 12 13 func2 = func.bind(someuser); 14 func2(); //输出foo
全局函数func通过someuser.func.bind将this指针绑定到了foo,调用func()输出了foo,我们试图将func2赋值为已绑定的func重新通过bind将this指针绑定到someuser的结果,而调用func2时的输出却竟然没有像我们想像中的那样变成by,这是为森么呢,继续看下去,让我为你揭秘
Bind方法的简化版(不支持绑定参数表)
1 someuser.func.bind = function (self) { 2 return this.call(self); 3 };
假设上面函数是someuser.func的bind方法的实现,函数体内的this指向的是someuser.func,因此函数也是对象,所以this.call(self)的作用就是以self作为this指针调用someuser.func
1 //将func = someuser.func.bind(foo) 展开 2 func = function () { 3 return someuser.func.call(foo); 4 }; 5 //再将func2 = func.bind(someuser) 展开 6 func2 = function () { 7 return func.call(someuser); 8 };
从上面的展开过程我们可以看出,func2实际上是以 someuser 作为func的this 指针调用了func,而func根本没有使用this指针,所以二次bind是没有效果的。
A5原型
原型是js面向对象中重要特性
1 function person() { 2 3 } 4 person.prototype.name = 'by'; 5 person.prototype.showName = function () { 6 console.log(this.name); 7 }; 8 9 var person = new person(); 10 person.showName();
上面这段代码使用了原型而不是构造函数初始化对象,这样做与直接在构造函数中定义属性有什么不同呢?
1构造函数内定义的属性继承方式与原型不同,子对象需要显示调用父对象才能继承构造函数内部定义的属性
2.构造函数内定义的任何属性,包括函数在内都会被重复创建,同一个构造函数产生的二个对象不共享实例
3构造函数内定义的函数有运行时闭包的开销,因为构造函数内的局部变量对其中定义的函数来说是可见的
下面的这段代码可以验证以上问题:
1 function foo() { 2 var inner = 'hello'; 3 this.prop1 = 'by'; 4 this.func1 = function () { 5 inner = ''; 6 }; 7 } 8 foo.prototype.prop2 = 'car'; 9 foo.prototype.func2 = function () { 10 console.log(this.prop2); 11 }; 12 var foo1 = new foo(); 13 var foo2 = new foo(); 14 console.log(foo1.func1 == foo2.func1); //输出false 15 console.log(foo1.func2 == foo2.func2); //输出 true
尽管如此,并不是说在构造函数内创建属性不好,而是二者各自有适合的范围,那么我们什么时候使用原型,什么时候使用构造函数来定义内部属性
1除非必须用构造函数闭包,否则尽量用原型定义成员函数,因为这样可以减少开销
2.尽量在构造函数内定义一般成员,尤其是对象或数组,因为用原型定义的成员是多个实例共享的
接下来,我们来介绍js的原型链机制
js中有二个特殊的对象:object与function,它们都是构造函数,用于生成对象,object.prototype是所有对象的祖先,function.prototype是所有函数的原型,包括构造函数,我把js中的对象分为三类:一类用户创建的对象,一类是构造函数对象,一类是原型对象。用户创建的对象,即一般意义上用new语句显示构造的对象,构造函数对象指的是普通的构造函数,即通过new调用生成普通对象的函数,原型对象特指构造函数prototype属性指向的对象,这三类对象中的每一类都有一个__p[roto__属性,它指向该对象的原型,从任何对象沿着它开始遍历都可以追溯到object.prototype,构造函数对象有prototype属性,指向一个原型对象,通过该构造函数创建对象时,被创建对象的__proto__属性将会指向构造函数的prototype属性,原型对象有constructor属性,指向它对应的构造函数,让我们通过下面的例子来理解原型
1 function foo() { 2 3 } 4 object.prototype.name = 'my'; 5 foo.prototype.name = 'bar'; 6 7 var obj = new object(); 8 var foo = new foo(); 9 console.log(obj.name); //输出my 10 console.log(foo.name); //输出bar 11 console.log(foo.__proto__.name); //输出bar 12 console.log(foo.__proto__.__proto__.name); //输出my 13 console.log(foo.__proto__.constructor.prototype.name); //输出bar
我们定义了一个叫做foo()的构造函数,生成对象foo.同时我们还分别给object和foo生成原型对象.
在js中,继承是以依靠一套叫做原型链的机制实现的,属性继承的本质就是一个对象可以访问到它的原型链上任何一个原型对象的属性,例如上例的foo对象,它拥有foo.__Proto__和foo.__proto__.proto__所有属性的浅拷贝(只复制基本数据类型,不复制对象),所以可以直接访问foo.constructor(来自foo.__proto__,即foo.prototype), foo.tostring(来自foo.__proto__.__proto__,即object.prototype).
A6对象的复制
js中没有像c语言一样的指针,也没有像java一样的clone方法可以进行对象赋值,因此我们需要手动实现这样一个函数,一个简单的做法就是复制对象的所有属性:
1 object.prototype.clone = function () { 2 var newobj = {}; 3 for (var i in this){ 4 newobj[i] = this[i]; 5 } 6 return newobj; 7 }; 8 var obj = { 9 name: 'by', 10 likes: ['node'] 11 }; 12 var newobj = obj.clone(); 13 obj.likes.push('python'); 14 15 console.log(obj.likes); //输出['node','python'] 16 console.log(newobj.likes); //输出['node','python']
上面的代码是一个对象浅拷贝的实现,即只复制基本类型的属性,而共享对象类型的属性,浅拷贝的问题是二个对象共享对象类型的属性,例如上例中的likes属性指向的是同一个数组
实现一个完全的复制,或深拷贝不是一件容易的事,因为除了基本数据类型,还有多种不同的对象,对象内部还有复杂的结构,因此需要用递归来实现
1 object.prototype.clone = function () { 2 var newobj = {}; 3 for (var i in this){ 4 if (typeof(this[i]) == 'object' || typeof(this[i] == 'function')){ 5 newobj[i] = this[i].clone(); 6 } else { 7 newobj[i] = this[i]; 8 } 9 } 10 return newobj; 11 }; 12 Array.prototype.clone = function () { 13 var newArray = []; 14 for (var i = 0; i< this.length;i++){ 15 if (typeof(this[i]) == 'object' || typeof(this[i] == 'function')){ 16 newArray[i] = this[i].clone(); 17 } else { 18 newArray[i] = this[i]; 19 } 20 } 21 return newArray; 22 }; 23 function.prototype.clone = function () { 24 var that = this; 25 var newfunc = function(){ 26 return that.apply(this, arguments); 27 }; 28 for (var i in this){ 29 newfunc[i] = this[i]; 30 } 31 return newfunc(); 32 }; 33 var obj = { 34 name: 'by', 35 likes: ['node'], 36 display: function(){ 37 console.log(this.name); 38 } 39 }; 40 var newobj = obj.clone(); 41 newobj.likes.push('python'); 42 console.log(obj.likes); //输出['node'] 43 console.log(newobj.likes); //输出['node','python'] 44 console.log(obj.display == newobj.display); //输出false
上面这个办法实现虽然看上去非常完美,它不仅递归的复制了对象复杂的结构,还实现了函数的深拷贝,这个方法在大多数的情况都好用,但是有一种情况它无能为力,例如下面的代码
var obj1 = {ref: null }; var obj2 = {ref: obj1 }; obj1.ref = obj2;
这段代码逻辑非常简单,就是二个相互引用的对象,当我们试图使用深拷贝来复制obj1和obj2中的任何一个时,问题就出现了,因为深拷贝的做法就是遇到对象就进行递归复制,那么结果只能无限循环下去,对于这种情况,简单的递归已经无法解决,必须设计一套图论算法,分析对象之间的依赖关系,建立一个拓扑结构图,然后分别依次复制每个顶点,并重新构建它们之间的依赖关系,这个我们暂且不讨论了,过于高深,而且在实际中我们也几乎不会遇到这种情况,好的,js全部重要的就在这里了,让我们下次再见!!!