jQuery源码学习-热身活动

提醒:本文发布于 2703 天前,文章内容可能 因技术时效性过期 或 被重新修改,请谨慎参考。

TOC
  1. 1. 前言
  2. 2. 回顾
    1. 2.1. 面向对象
      1. 2.1.1. 常见对象创建方式
      2. 2.1.2. 理解属性类型
    2. 2.2. 创建对象的模式
      1. 2.2.1. 工厂模式
      2. 2.2.2. 构造函数模式
      3. 2.2.3. 原型模式
      4. 2.2.4. 组合使用构造函数模式和原型模式
      5. 2.2.5. 动态原型模式
    3. 2.3. 继承
    4. 2.4. 借用构造函数
    5. 2.5. 组合继承
    6. 2.6. 原型式继承
    7. 2.7. 寄生式继承(类似于原型链继承)
    8. 2.8. 寄生组合式继承

前言

jQuery凭借简洁的语法和跨平台的兼容性,极大地简化了JavaScript开发开发人员遍历HTML文档、操作DOM、处理事件、执行动画和开发Ajax的操作….

然而我所在的公司里面并不推崇使用jq,尽管他们有jq的替代品(称之为pj),然而我在开发过程中,发现pj并没有想象的方便,甚至某种情况下有些鸡肋,也考虑过将自己平常常用的函数或者组件封装成自己的库,奈何本人能力渣渣,遂开jq源码学习系列,主要学习目的如下:

  • 屌炸天的思维/设计模式
  • 修炼js能力
  • 精妙的兼容处理

自勉.

回顾

在进入学习之前, 先复习一下一些可能便于理解jq源码的基本知识:

面向对象

面向对象就是你面对着你的对象……哦不,面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可 以创建任意多个具有相同属性和方法的对象…

单纯的文字貌似有点抽象 , 那还是直接用代码展示吧

常见对象创建方式

1)传统方式

var person = new Object();
person.name = "ThinkerChan";
person.age = 23;
person.job = "worker";
person.sayName = function(){
alert(this.name);
};

这种对象创建方式是比较古老的, 先new一个对象,然后再通过访问符来添加属性或者方法. 很快就被下面的字面量对象创建方式所代替:

2)字面量创建方式

var person = {
name: "ThinkerChan",
age: 23,
job: "worker",
sayName: function(){
alert(this.name);
} };

理解属性类型

事先声明, 这部分内容对处于日常开发的你来说并没什么很大用处, 写出来, 是因为它能更好地帮助理解对象这一概念

先看一个例子:

$('img').attr('width',100)

我们常常使用jq的时候, 会用到attr( )这个方法, attr也就是attribute, 也就是我们常常理解的属性 ,然而属性本身也有特性(property) , 特性的不同决定了属性的不同:

以下部分摘自Javascript高级程序设计第三版第六章

ECMAScript 中有两种属性:数据属性访问器属性
1) 数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的特性:

  • Configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
  • Enumerable:表示能否通过 for-in 循环返回属性。这个特性默认值为 true
  • Writable:表示能否修改属性的值。默认为true
  • Vaule:这个好理解,就是值, 默认undefined

2) 访问器属性
访问器属性不包含数据值;它们包含一对儿 gettersetter 函数(不过,这两个函数都不是必需的)。 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。同样有也 4 个描述其行为的特性:

  • Configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
  • Enumerable:表示能否通过 for-in 循环返回属性。这个特性默认值为 true
  • Get:在读取属性时调用的函数。默认为undefined
  • Set:在写入属性时调用的函数。默认为undefined

3) 修改特性(property)
ECMAScript 5 的 Object.defineProperty()方法。这个方法 接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属 性必须是:configurable、enumerable、writable 和 value。设置其中的一或多个值,可以修改 对应的特性值。

见demo:

var person = {};
// Object.defineProperty(对象,属性,特性对象)
Object.defineProperty(person, "name", {
writable: false, //已改成不可写
value: "Nicholas" //默认值"Nicholas"
});
alert(person.name); //"Nicholas"
person.name = "Greg"; //尝试重写name的值,失败,在严格模式下会报错
alert(person.name); //"Nicholas"

注意:
把 configurable 设置为 false,表示不能从对象中删除属性。如果对这个属性调用 delete,则 在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的, 就不能再把它变回可配置了。此时,再调用Object.defineProperty()方法修改除 writable 之外 的特性,都会导致错误:

见demo:

var person = {};
Object.defineProperty(person, "name", {
configurable: false,
value: "Nicholas"
});
//抛出错误
Object.defineProperty(person, "name", {
configurable: true,
value: "Nicholas"
});

即:可以多次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable特性设置为 false 之后就会有限制了

再看一个关于访问器属性修改特性的demo:

var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005; alert(book.edition); //2

相信这个例子能够很好地帮助你理解GetSet特性 .

注意:
不一定非要同时指定 gettersetter。只指定 getter 意味着属性是不能写,尝试写入属性会被忽略。 在严格模式下,尝试写入只指定了 getter 函数的属性会抛出错误。类似地,只指定 setter 函数的属性也 不能读,否则在非严格模式下会返回 undefined,而在严格模式下会抛出错误。

另外,旧版本的chrome, safari, opera都提供了实现对已ing功能的__defineGetter____defineSetter__

4) 定义多个属性

见demo:

var book = {};
//Object.defineProperties(对象,属性对象)
Object.defineProperties(book, {
_year: {
value: 2004
},
edition:{
value:2
}
}

5) 读取属性特性

见demo:

var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); //注意这里取的是实例属性
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"

对于数据属性_year,value 等于最初的值,configurable 是 false,而 get 等于 undefined。 对于访问器属性 year,value 等于 undefined,enumerable 是 false,而 get 是一个指向 getter 函数的指针。

创建对象的模式

前面说的两种对象创建方式都能创建对象,但是缺点也非常明显, 一旦需要创建很多对象, 那就得重复大量代码 , 于是有了以下几种创建对象的模式:

工厂模式

function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("k", 23, "worker");
var person2 = createPerson("r", 22, "worker");

工厂模式的显著特征就是将传统对象创建方式用函数封装起来, 即便在创建对象的时候没用显式地调用new,但在封装好的createPerson内还是清晰可见. 正如其名所言, 工厂模式的的局限在于每次创建的东西都是同一个模型 , 并不灵活, 也没用解决对象识别的问题 .

于是有了下一种模式

构造函数模式

function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}; }
var person1 = new Person("k", 23, "worker");
var person2 = new Person("r", 22, "worker");

构造函数模式有以下特点

  • 对象是new出来的
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句
  • 习惯性地将构造函数首字母大写

实际上, 构造函数内部创建对象的时候经历了”三部曲”

  1. 创建一个新对象, 并将this指向这个对象
  2. 执行构造函数内的代码(其实就是给对象添加属性)
  3. 返回这个对象

前面这个例子, 通过构造函数Person创建了两个不同的实例person1person2, 他们都有一个constructor属性,指向构造函数Person:

alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

提到类型识别对象识别就不得不说typeofinstanceof运算符,typeof常用语基本数据类型的识别上 , instanceof则用在对象实例的识别上:

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

由于javascript中一切皆对象 , 所以 很明显 person1person2都是Object的实例(继承的缘故).

注意事项
如果想要创建的实例中有某个方法(函数),那么这个方法(函数)不要放在构造函数内

见demo:

function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
//this.sayName=function(){ console.log(this.name);}
this.sayName = new Function("console.log(this.name)");
//注意: 上面的的声明方式和下面的new Function是等效的, 之所以这么做,是为了便于理解: 构造函数每次创建一个实例, sayName都会被重新创建一个匿名函数,仅仅是为了显示this.name而创建两个功能相同的函数,实在是浪费资源.
this.sayName2=sayName2; //应该采取这种方式
}
function sayName2(){
console.log(this.name);
}
var person1 = new Person('k',0,'worker');
var person2 = new Person('k2',0,'worker');
console.log(person1.sayName==person2.sayName); //false
console.log(person1.sayName2==person2.sayName2); //true

然而,问题又来了, 为了实现这个函数共享,我们将它放在了全局, 如果仅仅只是为了实现实例的某个功能,那么放在全局也是名不副实. 于是有了 原型模式

原型模式

依照javascript的特征, 每个函数都有一个prototype属性, 这是一个指针,指向一个对象,我们通常称之为 原型对象 , 可以理解成它是一个存储公共属性和方法的对象.

原型写法

见demo:

function Person(){
//...
}
Person.prototype.name = "k";
Person.prototype.age = 23;
Person.prototype.job = "worker";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"k"
console.log("Person.prototype: ",Person.prototype); //原型对象
console.log("Person.prototype.constructor==Person: ",Person.prototype.constructor==Person); //true, 指回构造函数
console.log("person1.constructor: ",person1.constructor); //构造函数
console.log("Person.constructor: ",Person.constructor); //思考一下这里是什么结果?
console.log("person1.prototype: ",person1.prototype); //undefined, 实例对象没有这个属性
console.log("person1.__proto__: ",person1.__proto__); //原型对象,确定实例和原型的关系(非标准) , 标准的属性是 [[prototype]],但不可见
person2.sayName(); //"k"
alert(person1.sayName == person2.sayName); //true

//屏蔽问题
person1.age = 100;
console.log(person1.age); //100
person1.age = null; // null会中断原型对应属性和实例的关系
console.log(person1.age); //null 说明优先查找实例中的属性,找到即停止
console.log(person2.age); //23

其它确定实例和原型之间的关系
1) isPrototypeOf

alert(Person.prototype.isPrototypeOf(person1));  //true
alert(Person.prototype.isPrototypeOf(person2)); //true

2) Object.getPrototypeOf //Es5方法

alert(Object.getPrototypeOf(person1) == Person.prototype); //true

查找属性
hasOwnProperty()检测属性是否在实例中, for-in循环则会遍历包括原型在内的所有属性.

要查找一个属性是否只存在原型中,可以封装一个这样的函数:

function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}

注意: for-in会把所有的属性给遍历出来,即便属性的Enumerable特性为flase,但在IE8之前,不会遍历.

如果要枚举所有可遍历的属性,怎用Object.keys() ,这是ES5的方法.

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"

备注: Object.keys()Object.getOwnPropertyNames()是用来替代for-in的.

优化后原型模式

function Person(name,age,job){

}
Person.prototype={
constructor:Person, //养成确定指向的习惯
name : "k",
age : 23,
job : "worker",
sayName : function(){
console.log(this.name)
}
}
var person1 = new Person();
console.log(Person.prototype.constructor); //如果没有constructor:Person , 结果就会是function Object, 说明原型的指向被更改了

原型的动态性
见demo:

var friend = new Person();
Person.prototype.sayHi = function(){ //注意:1)这里是先创建实例,再修改的原型 2)用的不是字面量方式
alert("hi");
};
friend.sayHi(); //"hi", 结果照样正常

demo2: 尝试字面量原型的动态性

function Person(){
//...
}
var person1 = new Person();
Person.prototype = {
constructor: Person,
name : "k",
age : 23,
job : "worker",
sayName : function () {
alert(this.name);
}
};
person1.sayName(); //报错 person1.sayName is not a function ,此原型(原生原型)非彼原型(字面量重写的原型)

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式 创建的.所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法.

但非常不建议修改原生对象的原型 .

原型模式的缺点(没错,优点也可以是缺点)

原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在 默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。 原型模式的最大问题是由其共享的本性所导致的。

见demo :

function Person(){

}
Person.prototype = {
constructor: Person,
name : "k",
friends:['k1','k2'] //数组是引用类型
};
var person1 = new Person();
var person2 = new Person();
var friends1 = person1.friends;
var friends2 = person2.friends;
person1.friends.push('k3');
console.log(friends1); // k1,k2,k3
console.log(friends2); // 注意这里变成了 k1,k2,k3

这就是为什么几乎没人单纯用原型模式.

解决方法 :

组合使用构造函数模式和原型模式

function Person(name,age){
this.name = name;
this,age = age;
this.friends=['k1','k2']; //什么?你不理解? 回想一下 new Function
}
Person.prototype = {
constructor: Person,
sayName : function(){
alert(this.name);
}
};
var person1 = new Person('k1',22);
var person2 = new Person('k2',22);
var friends1 = person1.friends;
var friends2 = person2.friends;
person1.friends.push('k3');
console.log(friends1); // k1,k2,k3
console.log(friends2); // k1,k2

这就是我们最常见的设计模式.
不过这看起来貌似又有点问题, 构造函数和原型分别分开,看起来很不爽, 于是就有了下面:

动态原型模式

function Person(name,age){
this.name = name;
this,age = age;
this.friends=['k1','k2'];
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}

这已经几乎完美了.

继承

许多 OO 语言都支持两种继承方式:接口继承和 实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名, 在 ECMAScript 中无法实现接口继承。

JS继承主要是通过原型链来实现的.

前面说了原型 , 原型链当然也很好理解, 打个可能不恰当的比喻 , 你有好几节水管,你想从厨房接到阳台, 但是哪一都段没法直接连接到你阳台, 因为每一节水管都太短了. 然而你可以将所有的水管一节一节的连起来, 这样就能达成目的了. 原型链就和这差不多: 对象a可以访问到原型A里面的属性和方法 ,假如原型A 又有一个原型B(也就是说原型A是原型B的一个实例), 这样的话对象a就能访问到原型B , 只要你喜欢,这个链能接更多…..

见demo :

function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType(); //这一步非常重要
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true, 说明访问到了SuperType原型
SubType.prototype.getSuperValue = function (){ //注意:1)没有使用字面量方式重写原型 2)这里的getSuperValue只是SubType原型里面的,而不是SuperType原型里面的
return false;
};
alert(instance.getSuperValue()); //false , 屏蔽了父类原型中同名属性(方法)

好, 那么问题来了:

function SuperType(){
this.arr = ['a','b']; //引用类型放在构造函数内, 妥妥的 :)
}
function SubType(){
//...
}
SubType.prototype = new SuperType();
var instance = new SubType();
var instance2 = new SubType();
instance.arr.push('c');
console.log(instance.arr); //["a", "b", "c"]
console.log(instance2.arr); //["a", "b", "c"]

到这里你可能疑惑, 我来给你解答 :
当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例, 因此它也拥有了一个它自己的 colors 属性等效于 我在SubType.prototype里面放置一个arr(引用类型)一样, 这就是原型链最大的问题 .

解决办法

借用构造函数

见demo:

function SuperType(){
this.arr = ['a','b'];
}
function SubType(){
SuperType.call(this) //借用了父类的构造函数,补充说明,函数也是对象,对象就有属性和方法, call就是函数的方法之一.
}
var instance = new SubType();
var instance2 = new SubType();
instance.arr.push('c');
console.log(instance.arr); //["a", "b", "c"]
console.log(instance2.arr); //["a", "b"]

注意事项:
1) 传递参数

function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this, "k"); //继承了 SuperType,同时还传递了参数
this.age = 23;
}
var instance = new SubType();
alert(instance.name); //"k";
alert(instance.age); //23

2) 问题

function SuperType(){
this.arr = ['a','b'];
}
SuperType.prototype={
constructor:SuperType,
name:'k',
show:function(){
console.log(this.arr);
}
}
function SubType(){
SuperType.call(this)
}
var instance = new SubType();
var instance2 = new SubType();
instance.arr.push('c');
console.log(instance.arr);
console.log(instance2.arr);
instance.show(); //报错

从上面例子可以看出, 如果仅仅使用借用构造函数实现继承的话, 父类原型中的属性和方法显然是不可见的, 那么问题又回到了构造函数模式存在的问题–函数复用的问题 .

于是解决方法如下:

组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承.这样,既通过在原型上定义方法实现了函数 复用,又能够保证每个实例都有它自己的属性.

思考一下, 原型链继承借用构造函数单独使用的时候都有问题, 组合在一起的时候问题就没了, 期中肯定发生了重写.

function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name); //第二次调用父类, 属性被重写
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用父类
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};

原型式继承

见demo:

function object(o){
function F(){}
F.prototype=o;
return new F()
}
//问题:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"] // 会被共享
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

寄生式继承(类似于原型链继承)

function object(o){
function F(){}
F.prototype=o;
return new F()
}
function createAnother(original){
var clone=object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){
alert("hi");
}
};
return clone;

寄生组合式继承

访客评论