可以在$和jQuery对象挂在在window中,实现全局引用。
给Windows对象扩展一个$的属性,让它拿到jQuery构造函数的引用
可以用$访问到jQuery的构造函数
// jQuery.js
(function(root) {
var jQuery = function(){}
root.$ = root.jQuery = jQuery;
})(this)
调用$()方法的目的是创建一个实例,构造函数直接调用是当做一个普通的函数来处理的
// jQuery.js
(function(root) {
var jQuery = function() {}
})(this)
具体代码:
// jQuery.js
(function(root) {
var jQuery = function() {
console.log('生成一个实例对象');
};
root.$ = root.jQuery = jQuery;
})(this)
①构造函数也是一个普通函数,创建方式和普通函数一样,但习惯上首字母大写(小写也是可以的),用new关键字调用
②和普通函数的区别:
1)调用方式不一样
普通函数调用:直接调用,如,person()
构造函数调用:使用new关键字调用,如,new Person()
2)作用不同
构造函数用来新建实例对象
③构造函数的函数名和类名相同:Person()这个构造函数,Person既是函数名,也是这个对象的类名
④内部用this来构造属性和方法
function Person(name, gender, age) {
this.name = name;
this.gender = gender;
this.age = age;
this.sayHi=function(){
alert("Hi")
}
}
当创建上面的函数以后, 我们就可以通过 new 关键字调用,也就是通过构造函数来创建对象了
var p1 = new Person('张三', '男', 14); // 创建一个新的内存 #f1
var p2 = new Person('李四', '女', 28); // 创建一个新的内存 #f2
⑤构造函数的执行流程
A、立刻在堆内存中创建一个新的对象
B、将新建的对象设置为函数中的this
C、逐个执行函数中的代码
D、将新建的对象作为返回值
其他说法:
1、当以new关键字调用时,会创建一个新的内存空间 #f1,并标记为Person的实例
2、函数体内部的this指向该内存空间 #f1
以上两步:每当创建一个实例的时候,就会创建一个新的内存空间(#f1,#f2),创建#f1的时候,函数体内部的this指向#f1,创建#f2的时候,函数体内部的this指向#f2
3、执行函数体内的代码
给this添加属性,就相当于给实例添加属性
4、默认返回this
由于函数体内部的this指向新创建的内存空间(默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1),而该内存空间(#f1)又被变量p1所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。
⑥普通函数:因为没有返回值,所以为undefined
function person(){
}
var per = person()
console.log(per) // undefined
⑦构造函数
1)默认返回this
function Person1(){
this.name = 'zhangsan';
}
var p1 = new Person1()
console.log(p1) // Person1 {name: "zhangsan"}
console.log(p1.name) // zhangsan
首先,当用 new 关键字调用时,产生一个新的内存空间 #f11,并标记为 Person1 的实例;
接着,函数体内部的 this 指向该内存空间 #f11;
执行函数体内部的代码;
由于函数体内部的this 指向该内存空间,而该内存空间又被变量 p1 所接收,
所以 p1 中就会有一个 name 属性,属性值为 ‘zhangsan’。
2)手动添加一个基本数据类型的返回值,最终还是返回 this
function Person2() {
this.age = 28;
return 50;
}
var p2 = new Person2();
console.log(p2) // Person2 {age: 28}
console.log(p2.age); // 28
如果上面是一个普通函数的调用,那么返回值就是 50
例一:
function Person3() {
this.height = '180';
return ['a', 'b', 'c'];
}
var p3 = new Person3();
console.log(p3.height); // undefined
console.log(p3.length); // 3
console.log(p3[0]); // 'a'
例二:
function Person4() {
this.gender = '男';
return { gender: '中性' };
}
var p4 = new Person4();
console.log(p4.gender); // '中性'
⑧不用new关键字,直接运行构造函数,是否会出错?如果不会出错,那么,用new和不用new调用构造函数,有什么区别?
1)用new调用构造函数,函数内部会发生如下变化:
创建一个this变量,该变量指向一个空对象。并且该对象继承函数的原型;
属性和方法被加入到this引用的对象中;
隐式返回this对象(如果没有显性返回其他对象)
用伪程序来展示上述变化:
function Person(name){
// 创建this变量,指向空对象
var this = {};
// 属性和方法被加入到this引用的对象中
this.name = name;
this.say = function(){
return "I am " + this.name;
}
// 返回this对象
return this;
}
var person1 = new Person('nicole');
person1.say(); // "I am nicole"
可以看出,用new调用构造函数,最大特点为,this对象指向构造函数生成的对象,
所以,person1.say()会返回字符串: “I am nicole”。
注意:如果指定了返回对象,那么,this对象可能被丢失。
function Person(name){
this.name = name;
this.say = function(){
return "I am " + this.name;
}
var that = {};
that.name = "It is that!";
return that;
}
var person1 = new Person('nicole');
person1.name; // "It is that!"
2)直接调用函数
如果直接调用函数,this对象指向window,并且,不会默认返回任何对象(除非显性声明返回值)。
var person1 = Person('nicole');
person1; // undefined
window.name; // nicole
按照正常思维,函数里面直接return一个new jQuery即可:
// jQuery.js
(function(root) {
var jQuery = function() {
return new jQuery();
};
root.$ = root.jQuery = jQuery;
})(this)
通过new的方式去创建jQuery的实例,实际上会做两步操作:
1、创建一个object对象
2、调用本身的构造函数
这样会引发死循环的问题,因为该函数在不断地创建一个新对象,不断的调用构造函数
那么jQuery是如何设计的呢?我们来看下jquery的共享原型设计图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlwgJ7K6-1577168361128)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567079008264-e9368f7c-41ec-4b66-bbed-a5ad0e90a2ba.png#align=left&display=inline&height=1328&name=jquery%E5%85%B1%E4%BA%AB%E5%8E%9F%E5%9E%8B%E8%AE%BE%E8%AE%A1%E5%9B%BE.png&originHeight=1328&originWidth=2376&size=505896&status=done&width=2376)]
从图中可以看出,当我们调用$的时候,实际上是在调用jQuery原型上的init方法,把init方法当成一个构造函数,然后返回它的实例对象。这样会产生一个问题,就是我们实际是要创建jQuery对象,并访问jquery原型上扩展的属性和方法。如何解决这个问题?jquery用了一个共享原型的设计,那就是将jQuery原型上的init构造函数和jQuery本身共享一个原型对象。
具体代码如下:
// jQuery.js
(function(root) {
var jQuery = function() {
// 如果想直接调用$()方法,我们尝试直接return
// 但这种写法是错误的,如果直接return本身,会造成死循环
// return new jQuery();
return new jQuery.prototype.init;
};
给jQuery的原型扩展了一个init的方法
jQuery.prototype = {
init: function() {
}
}
// 共享原型对象
找到jQuery原型对象上的init方法,把它当做一个构造函数,
设置它的原型对象指向jQuery的原型对象,达到共享原型的目的
jQuery.prototype.init.prototype = jQuery.prototype;
root.$ = root.jQuery = jQuery;
})(this)
这样我们打印console.log($()),就可以看到jquery原型对象的init方法了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWBArZFf-1577168361138)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567079723886-e9ea1980-6a09-42d3-b1d5-515a1262d28b.png#align=left&display=inline&height=201&name=image.png&originHeight=402&originWidth=1028&size=58076&status=done&width=514)]
这时候的 ( ) 实 际 指 向 的 是 j Q u e r y 原 型 上 的 i n i t 方 法 , 而 i n i t 的 原 型 和 j Q u e r y 本 身 的 原 型 共 享 一 个 原 型 对 象 , 这 样 往 j Q u e r y 上 扩 展 的 方 法 就 可 以 通 过 ()实际指向的是jQuery原型上的init方法,而init的原型和jQuery本身的原型共享一个原型对象,这样往jQuery上扩展的方法就可以通过 ()实际指向的是jQuery原型上的init方法,而init的原型和jQuery本身的原型共享一个原型对象,这样往jQuery上扩展的方法就可以通过()获取。
例如我们在jQuery的原型上扩展一个css的方法:
// jQuery.js
jQuery.prototype = {
init: function() {
},
css: function() {
}
}
jQuery的核心函数extend,用于对对象进行扩展,一般有三种方式:
1、对任意对象进行扩展,参数必须是两个及两个以上
第一个对象没有事扩展,有,是重新赋值
// demo.html
$.extend({}, { name: 'arbor'});
2、对jQuery本身扩展属性或者方法
// demo.html
$.extend({
work: function() {
}
})
jQuery.work();
3、对jQuery实例对象扩展属性或方法
// demo.html
$.fn.extend({
sex: '男'
})
$().sex;
这里的$.fn_指的是jQuery的prototype原型对象_
那如何实现呢?
很简单,就是给jQuery扩展一个fn的方法,去等于jQuery的原型对象
// jQuery.js
jQuery.fn = jQuery.prototype = {
init: function() {
},
css: function() {
}
}
// 当然,共享原型对象也得写成:
// 共享原型对象
jQuery.fn.init.prototype = jQuery.fn;
下面,正题来了,核心函数extend是如何实现的呢?
首先需要保证 . f n 和 .fn和 .fn和.都可以调用到,其次_在jQuery的原型对象和jQuery对象上新建了同一个extend的函数_
// jQuery.js
jQuery.fn.extend = jQuery.extend = function() {
}
如何判断是给什么进行扩展属性和方法呢?
通过参数来判断
A、给任意对象扩展,参数必须是两个及两个以上
B、给jQuery本身或者jQuery实例对象扩展,只需要传递一个参数
不管是哪种,第一个参数必须是object
**
其实,我们分析extend函数的调用方式,无外乎两种情况,一种是给任意对象扩展,从第二个参数开始作为对第一个参数的补充,一种是直接扩展,既然这样,我们就可以对第一个参数做文章了:
1、如果第一个参数是个对象,那么我们就可以把从第二个开始的参数进行遍历,然后填充到第一个参数中。这里可以用到arguments;
2、如果第一个参数不是对象,那么就简单了,直接把参数扩展到_jQuery对象或者jQuery的实例对象上即可,因为参数只有一个。这里要特别注意的是,this的指向问题,this是指向jQuery对象本身?还是jQuery的实例对象上?什么意思呢?当你往jquery对象上扩展一个方法或者属性时,这时候传参只有一个,那是不是要告诉程序,往哪里扩展?是往jQuery上扩展呢?还是往jQuery实例的原型对象扩展呢?详见代码18-24行。
既然如此,我们就来看看代码是如何实现的。
// 浅拷贝
jQuery.fn.extend = jQuery.extend = function() {
// 定义一个变量target,用于存储传入的第一个值,如果值为空,那默认为空对象的引用
/*
* max
* 通过arguments获取用户调用时传递的参数,获取第一个参数,如果没有传就创建一个对象并赋值给一个变量
* 此时通过arguments[0]获取用户传递过来的第一个参数,但是并没有100%保证这个参数一定是一个object
* 就要进行数据类型的判断:见
* */
var target = arguments[0] || {}
// 定义一个变量len,用于存储传参的长度
var len = arguments.length;
// 定义一个遍历i,
var i = 1;
// option 第二个参数后面的参数遍历过程中单个的存储
var option;
// 判断参数一的类型,如果不是object就创建一个对象并赋值给它
if (target !== 'object') {
target = {};
}
// 如果参数只有一个,那么就是对jQuery对象或者jQuery的实例对象扩展
// 例如: 1.jQuery对象的扩展 $.extend({ work: function() {}})
// 2.jQuery的实例对象扩展 $.fn.extend({ sex: '男' })
if (length === i) {
// 这里的this等于函数调用的对象本身,即jQuery对象或者jQuery的实例对象
target = this;
}
// 如果参数不止一个,给任意对象进行扩展的逻辑,例如$.extend({}, {name: 'arbor'});
// 也就是浅拷贝
for(; i < len; i++) {
if ((option = arguments[i]) !== null) {
for (key in option) {
// target指的是第一个参数对象,进行扩展
target[key] = option[key];
}
}
}
return target; // 一定要有返回值,不然是undefined
}
我们可以验证一下:
<script>
var ret = { name: 'arbor', list: { age: 30 }};
// 给任意对象扩展
var obj = $.extend({}, ret);
console.log(obj)
</script>
执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDJTfG6p-1577168361140)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567134677301-1565f953-7af9-494d-a980-6f4c141484ca.png#align=left&display=inline&height=72&name=image.png&originHeight=144&originWidth=1024&size=24784&status=done&width=512)]
到这里,其实extend的基本逻辑已经捋清楚了。但是,如果代码是这样子的呢?结果是否符合我们的预期?
<script>
var ret = { name: 'arbor', list: { age: 30 }};
var res = { list: { sex: 'male' }};
// 给任意对象扩展
var obj = $.extend(true, {}, ret, res);
console.log(obj)
</script>
执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1aPjf04-1577168361141)(https://cdn.nlark.com/yuque/0/2019/png/467105/1567135747923-e70ff4ef-719d-4ffd-bd36-4e4f3ef0c4ef.png#align=left&display=inline&height=67&name=image.png&originHeight=134&originWidth=1018&size=24585&status=done&width=509)]
从执行结果来看,按照正常思维,list对象里面应该是包含有age和sex。那是否需要一个参数去控制呢?答案是肯定的,jQuery中就提供了一种接口调用方法,当第一个对象是布尔类型时,且等于true的时候,就会去执行深拷贝,而我们上面代码中的for循环其实只是一种浅拷贝。那么,我们来想想要如何实现?
1、需要对传参的第一个变量进行判断,是否为布尔类型,如果是布尔类型,我们就需要将第二个参数作为需要扩展的对象,然后从第三个参数开始遍历,对第二参数进行扩展;
2、在遍历过程中,在上述浅拷贝的代码基础上,需要判断对象中是否已经有当前的key了,如果没有,直接object[key] = value即可,如果有,那么要拿到拥有key的对象再次去递归循环第二层。什么意思呢?其实就是在上述例子中,要使用res对第一个变量进行扩展,会发现变量上已经有list了,那就把{ sex: ‘male’ }拿到做第二次递归拷贝,看不懂没事,等会我们会分析整个执行的过程。
3、在拷贝过程中,甚至jQuery的整个实现过程中,我们可能会抽象出来一些工具类,例如判断是否为对象,或者是否为数组等,这时候就可以利用extend方法往jQuery上附加方法了。
而之说以说extend是jQuery的核心函数,这里就体现出来了:jQuery几乎所有的模块和函数都是通过extend函数扩展的。
浅拷贝只拷贝第一层,如果是基本类型就是拷贝,否则就是引用(后者会在对象修改属性时互相影响)
深、浅拷贝是针对对象、数组来说的,深拷贝是创建一个新的对象或数组,然后把需要拷贝的属性或元素添加至新建的对象或数组上,相当于把要拷贝的信息复制了一份。浅拷贝是把属性指向要拷贝的对象或数组。深拷贝的对象修改后,不会影响原对象,浅拷贝则会。
用通俗的例子来说,深拷贝就像是把张三克隆了一下,浅拷贝是给张三取了个小名,比如小张。假如克隆人摔倒了,张三不会受伤,因为他们是两个人。但如果小张摔倒了,张三也会受伤,因为他们是同一个人,只是取了两个名字而已
好了,到这里,那我们来看看代码是如何实现的吧:
// demo.html
<script>
// 深拷贝、浅拷贝
// 特征:第一个参数是布尔类型,true、false,否则就是浅拷贝
var ret = {name:'菠菜',list:{age:10}}
var res = {list:{sex:'女'}}
/*浅拷贝:只做替换的关系
* 在遍历ret的时候会给{}扩展一个list的属性,并把它的值赋值过去
* 当遍历res的时候,会找到list的属性,并把指向的对象的引用替换掉{}里面list属性的值的引用
*/
var obj = $.entend({},ret,res) //obj: {name:'菠菜',list:{sex:'女'}}
// 深拷贝
var obj = $.entend(true,{},ret,res)
</script>
// jQuery.js
(function(root) {
jQuery.fn.extend = jQuery.extend = function() {
// 拿到第一个参数对象
var target = arguments[0] || {}
var length = arguments.length;
// 对任意对象进行扩展,可忽略第一个对象,因为从第二个参数开始,都是作为第一个对象的扩展
var i = 1;
// deep变量是用于对第一个参数进行判断,是进行深拷贝(追加)还是浅拷贝(覆盖)
var deep = false;
// option 需扩展对象{}后面的参数对象
// key 参数对象的key
// copy 参数对象的value
// src 是判断需扩展对象上是否已存在key
// copyIsArray 是防止多调用一次判断是否为数组的方法,简化代码
var option, key, copy, src, copyIsArray, clone;
if (typeof target === 'boolean') {
// 判断深浅拷贝
deep = target;
// 将需要扩展的对象赋值为第二个
target = arguments[1];
// 循环从第三个开始
i = 2;
}
if (typeof target !== 'object') {
target = {};
}
// 如果参数只有一个,那么就是对jQuery对象或者jQuery的实例对象扩展
// 例如: 1.jQuery对象的扩展 $.extend({ work: function() {}})
// 2.jQuery的实例对象扩展 $.fn.extend({ sex: '男' })
if (length === i) {
// 这里的this等于函数调用的对象本身,即jQuery对象或者jQuery的实例对象
target = this;
// 这里的用意是拿到jQuery内置的一些方法,例如:
i--;//让i的值变为0
}
// 深浅拷贝
// 给任意对象进行扩展的逻辑,例如$.extend({}, {name: 'arbor'});
for(; i < length ; i++) {
// option:需要遍历的属性的值
if ((option = arguments[i]) !== null) {
for(key in option) {
// 将第二个参数之后的值赋值给第一个对象
// 注意如果有多个参数,参数的key相同的话,会实现覆盖
// target[key] = option[key];
copy = option[key];
// 首次遍历的时候src可能是undefined,因为第二个参数值可能是空{}
src = target[key];
// 判断是深浅拷贝,有值是深拷贝,并且后面的参数是Object还是Array类型
if (deep && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {// 如果是数组
// 每次都要重置
copyIsArray = false;
// src有值并且是数组就直接赋值,否则就创建空数组
clone = src && jQuery.isArray(src) ? src : [];
} else {// 如果是对象
// src有值并且是对象就直接赋值,否则就创建空对象
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// 做一次浅拷贝
target[key] = jQuery.extend(deep, clone, copy);
} else if (copy !== undefined) { //判断浅拷贝是否有值
target[key] = copy;
}
}
};
}
return target;
}
// 共享原型对象
jQuery.fn.init.prototype = jQuery.fn;
jQuery.extend({
// 类型检测的方法
isPlainObject: function(obj) {
return toString.call(obj) === '[object Object]';
},
isArray: function(obj) {
return toString.call(obj) === '[object Array]';
}
})
root.$ = root.jQuery = jQuery;
})(this)
注意到36行的i–没有,如果我们是往jQuery本身附加一些属性或者方法时,循环是需要从下标为0开始的,而不能从初始化的下标为1开始。
62行是核心代码,就是对存在多层级的对象或者数组递归地去拷贝,从而达到深拷贝的效果。
最后记得return出来,否则你拿到的肯定是undefined。
最后,我们再来捋一下执行过程吧:
// for循环中
deep = true;
i = 2;
// 1.首层循环
option = ret = { name: 'arbor', list: { age: 30 }};
// 1.1循环option对象的key和value,即name: 'arbor'
key = 'name'
copy = option[key] = 'arbor';
src = target[key] = undefined;
// 判断走else-if
target[key] = copy
// 此时的target的值
target: {
name: 'arbor',
}
// 1.2 for..in继续循环list: { age: 30 }
key = 'list'
copy = option[key] = { age: 30 };
src = target[key] = undefined;
// 判断走if
copyIsArray = false;
clone = {};
// 递归执行jQuery.extend,进行浅拷贝
target[key] = jQuery.extend(true, {}, { age: 30 }) = { age: 30 }
// 此时的target的值, 执行1.2.1得到的值
target: {
name: 'arbor',
list: { age: 30 }
}
// 1.2.1 list
option = { age: 30 }
key = 'age'
copy = option[key] = 30;
src = target[key] = undefined;
// 判断走else-if
target[key] = copy
// 此时的target的值
target: {
age: 30,
}
// 子循环结束,return target
// 2.首层循环
deep = true;
i = 2;
option = res = { list: { sex: 'male' }};
// 2.1 for..in继续循环list: { sex: 'male' }
key = 'list'
copy = option[key] = { sex: 'male' };
src = target[key] = { age: 30 };
// 判断走if
copyIsArray = false;
clone = { age: 30 };
// 递归执行jQuery.extend,进行浅拷贝
target[key] = jQuery.extend(true, { age: 30 }, { sex: 'male' }) = { sex: 'male' }
// 此时的target的值, 执行2.1.1得到的值,此时return最终的target
target: {
name: 'arbor',
list: { age: 30, sex: 'male' }
}
// 2.1.1 list
option = { sex: 'male' }
key = 'sex'
copy = option[key] = 'male';
src = target[key] = undefined;
// 判断走else-if
target[key] = copy
// 此时的target的值
target: {
sex: 'male',
}
// 子循环结束,return target
至此,jQuery的整体架构和核心功能就到一段落了。