代码要写成别人看不懂的样子(十五)

本文介绍了一种链式调用的设计模式,通过返回当前对象的方法实现对同一对象的多个方法链式调用。同时展示了如何利用该模式实现一个简易的jQuery框架,包括元素选择、CSS样式设置、事件绑定等功能。

本篇文章参考书籍《JavaScript设计模式》–张容铭

前言

  大家一定用过 JQuery 吧,这东西的一大特性就是链式调用,获取到某个元素后,直接连续执行多个方法,贼帅,其实我们也可以做到。

  本节开始,就给大家介绍一些技巧性的设计模式,平时能经常用到,类似语法糖一样,简单易懂,而且功能奇妙,不会像前面的设计模式那样长篇大论,肯定比前面容易接收,而且有趣。

链模式

   通过对象方法中,将当前对象返回,实现对同一个对象多个方法的链式调用

  大家看一下下面的代码:

var A = function() {}
A.prototype = {
	length: 2,
	size: function() {
		return this.length;
	}
}

  我们创建了一个对象 A , 并且 A 的原型对象上拥有一个属性和一个 size 方法,那么我想访问这个 size 方法,应该怎么做?

  方法A在原型上,可以通过关键字 new 创建对象访问,如下:

var a = new A();
consoele.log(a.size()); //2

  上面是正确的访问方法,如果按照下面的方式访问,程序就会报错。

console.log(A.size());
console.log(A().size());

  上面第一种报错是因为, size 是绑在 A 的原型上的,而不是 A 上,第二种方式报错是因为执行完 A 后,没有返回值,所以找不到 size 方法。但是在 JQuery 中就可以访问到。

   JQuery 是通过 $() 的方式进行链式调用的,说明在 $() 函数执行结束后,返回了一个但有很多方法的对象。原理弄清楚了,接下来实现一下。

var A = function() {
	return A.fn;
}
A.fn = A.prototype = {
	length: 2,
	size: function() {
		return this.length;
	}
}

  到这里会遇到一个新问题,我们知道 JQuery 的目的是为了获取元素,返回的是一组元素簇(元素的聚合对象),但是现在返回的却是一个 A.fn 对象,显然达不到我们的需求,所以如果 A.fn 能提供给我们一个获取元素的方法 init 就好了,我们可以将 init 方法获取到的元素在 A 方法中返回。

var A = function(selector) {
	return A.fn.init(selector);
}
A.fn = A.prototype = {
	init: function(selector) {
		//将元素所谓一个属性赋值给fn
		this[0] = document.getElementById(selector);
		//矫正length属性
		this.length = 1;   
		return this;
	},
	length: 2,
	size: function() {
		return this.length;
	}
}

var demo = A('demo');
console.log(demo); //Object{0: div#demo, init: function, length: 1, size: function}
console.log(A('demo').size()) //1

  上面的代码还是有问题,当我们再获取一个 id 不为 demo 的元素的时候,后面会覆盖掉前面的代码。这是因为对象是引用数据类型,解决这个问题也很容易,使用 关键字 new 就可以。

  但是这又会导致调用 size 方法的时候报错。

  这个问题是因为我们通过 new 对对象内的属性复制了, this 的指向就不是 A.fn 了而是 A.fn.A.init

   为什么this会是 A.fn.A.init ?

  这个问题就要从构造函数说起了,各位听我慢慢道来哈~

  我们知道 new 关键字执行的实质是对构造函数的属性进行一次复制,那么 new A.fn.init(selector) 的构造函数就可以表示成 A.fn.init = A.init ,我们将 A.init 带入到 A.fn.init 中的 init ,就可以得到 A.fn.A.init 的结果了。

   回到我们上面遇到的那个问题,在使用 new 关键字了之后,this不再是 A.fn 了那我们怎么解决一下?

  这个问题 JQuery 中有一个很巧妙的解决方案,只要将构造函数的原型指向一个已经存在的对象即可。

A.fn.init.prototype = A.fn;

  这是因为实例化的对象是在构造函数执行时创建的,所以 constructor 指向的就是 A.fn.A.init 构造函数,但是这个对象在执行完毕之后就不存在了,所以我们为了强化 constructor 可以按照如下做法:

var A = function(selector) {
	return new A.fn.init(selector);
}
A.fn = A.prototype = {
	//强化构造器
	constructor: A,
	init: function(selector) {
		console.log(this.constructor)
		/**
		* 输出结果
		* fucntion(selector) {
		* 	return new A.fn.init(selector);
		* }
		*/
		...
	}
	...
}
A.fn.init.prototype = A.fn;

  现在 A 框架的 _ _ proto _ _ A 了, size 方法也能正常获得了。

获取一类元素

  上面我们能获取带有 id 元素的链式方法了,现在我们尝试获取某一类元素,别的不用动,只需要改 init 方法就行。

//selector 选择符, context 上下文
var A = function(selector, context) {
	return new A.fn.init(selector, context);
}
A.fn = A.prototype = {
	constructor: A,
	init: function(selector, context) {
		//获取元素长度
		this.length = 0;
		//默认获取元素上下文为 document
		context = context || document;
		//如果是 id 选择符 按位非将-1转为0, 转化为布尔值 false
		if(~selector.indexOf('#')) {
			//截取 id 并选择
			this[0] = document.getElementById(selector,slice(1));
			this.length = 1;
		//如果是元素名称
		} else {
			//在上下文中选择元素
			var doms = context.getElementsByTagName(selector),
				i = 0,
				len = doms.length;
			for(; i< len; i++) {
				//压入this中
				this[i] = doms[i]
			}
			//矫正长度
			this.length = len;
		}
		//保存上下文
		this.context = context;
		//保存选择符
		this.selector = selector;
		//返回对象
		return this;
	},
	...
}

数组与对象

  如果大伙研究过 JQ 的源码,会发现, JQ 获取的元素更像一个数组,而我们的 A 框架返回的却是一个对象,这是由于 JS 是若语言类型,数组,函数,对象,都是对象的实例,所以是没有纯粹的数组类型的,浏览器在判断是否是数组的时候会判断 length 属性,能否通过 [索引值] 访问,是否有数组方法等。

  所以我们可以给 A.fn 中增加几个数组方法,来欺骗浏览器。

A.fn = A.prototype = {
	//...
	//增强数组
	push: [].push,
	sort: [].sort,
	splice: [].splice
}

方法拓展

   JQ 中 很多方法都可以通过点语法链式使用,那这些方法我们因该怎么扩展呢?

   JQ 的做法是定义了一个 extend 方法, jQueryUI 就是通过它拓展的,有时我们对对象拓展也会用到它,所以 extend 有两个作用,一是外部对下那个拓展,二是内部对象拓展。根据这个原理,我们可以简单实现 extend 方法:

  如果只有一个参数我们就定义对 A 对象或者 A.fn 的拓展,对 A.fn 的拓展是因为我们使用 A() 返回对象中的方法是从 A.fn 上获取的。多个参数表示对第一个对象的拓展。

//对象拓展
A.extend = A.fn.extend = function() {
	//拓展对象从第二个参数算起
	var i = 1,
		len = arguments.length,
		//第一个参数为源对象
		target = arguments[0],
		//拓展对象中属性
		j;
	//如果只传一个参数
	if(i == len) {
		//源对象为当前对象
		target = this;
		//i从0计数
		i--}
	//遍历参数中拓展对象
	for(; i < len; i++) {
		//遍历拓展对象中的属性
		for(j in arguments[i]) {
			//拓展源对象
			target[j] = arguments[i][j];
		}
	}
	//返回源对象
	return target;
}

//测试
//拓展一个对象
var demo = A.extend({first: 1}, {second: 2}, {third: 3});
console.log(demo); //{first: 1, second: 2, third: 3}

//拓展 A.fn 方式一
A.extend(A.fn, {version: '1.0'});
console.log(A('demo').version)    //1.0
//拓展 A.fn 方式二
A.fn.extend({getVersion: function() {return this.version;}});
console.log(A('demo').getVersion())    //1.0

//拓展 A 方式一
A.extend(A, {author: '不见星空'});
console.log(A.author)    //不见星空
//拓展 A 方式二
A.extend({nickName: '不见星空'});
console.log(A.nickName)    //不见星空

添加方法

  接下来我们正式的为 A 框架添加元素绑定事件 on ,设置 CSS 方法,设置元素属性方法 attr ,设置元素内容方法 html

A.fn.extend({
	//添加事件
	on: (function() {
		//标准浏览器DOM2级事件
		if(document.addEventListener) {
			return function(type, fn) {
				var i = this.length - 1;
				//遍历所有元素添加事件
				for(; i>= 0; i--) {
					this[i]/addEventListener(type, fn, false);
				}
				//返回源对象
				return this;
			}
		//IE浏览器DOM2级事件
		} else if(document.attachEvent) {
			return function(type, fn) {
				var i = this.length - 1;
				for(; i >= 0; i--) {
					this[i].addEvent('on' + type, fn);
				}
				return this;
			}
		//不支持DOM2级事件浏览器添加事件
		} else {
			return function(type, fn) {
				var i = this.length - 1;
				for(; i >= 0; i--) {
					this[i]['on' + type] = fn;
				}
				return this;
			}
		}
	}) ()
})

  获取或设置 css 样式方法中,如果只传递一个参数,如果参数是字符串,则返回第一个元素 css 样式值,此时不能进行链式调用。如果是对象则为每一个元素设置多个 css 样式,如果是两个参数则为每一个元素设置样式。

A.extend({
	//设置css样式
	css: function() {
		var arg = arguments,
			len = arg,length;
		if(this.length < 1) {
			return this;
		}
		//只有一个参数时
		if(len === 1) {
			//如果为字符串则为获取第一个元素 CSS 样式
			if(type arg[0] === 'string') {
				//IE
				if(this[0].currentStyle) {
					return this[0].currentStyle[name]
				} else {
					return getComputedStyle(this[0], false)[name];
				}
			//为对象时则设置多个样式
			} else if(typeof arg[0] === 'object') {
				for(var i in arg[0]) {
					for(var j = this.length - 1; j >= 0; j--) {
						//调用拓展方法 camelCase 将‘-’分割线转化为驼峰式
						this[j].style[A.camelCase(arg[0])] = arg[0][i];
					}
				}
			}
		//两个参数设置一个样式
		} else if(len === 2) {
			for(var j = this.length - 1; j >= 0; j--) {
				this[j].style[A.camelCase(arg[0])] = arg[1];
			}
		}
		return this;
	}
})

  获取元素属性的方法与设置 css 样式方法一样,如果只传一个参数,如果参数为字符串,则返回第一个元素属性值,此时不能再链式调用,如果参数是对象则设置每一个元素的多个属性值,如果传递两个参数,则第一个参数为属性名,第二个参数为属性值,设置每个元素的属性。

A.fn.extend({
	//设置属性
	attr: function() {
		var arg = arguments,
			len = arg.lenght;
		if(this.length < 1) {
			return this;
		}
		//如果一个参数
		if(len === 1) {
			//为字符串则获取第一个元素属性
			if(typeof arg[0] === 'string') {
				return this[0].getAttribute(arg[0]);
			//为对象设置每个元素的多个属性
			} else if(typeof arg[0] === 'object') {
				for(var i in arg[0]) {
					for(var j = this.length - 1; j >=0; j--) {
						this.[j].setAttribute(i, arg[0][i]);
					}
				}
			}
		//两个参数则设置每个元素单个属性
		} else if(len === 2) {
			for(var j = this.length - 1; j >= 0; j--) {
				this[j].setAttribute(arg[0], arg[1]);
			}
		}
		return this;
	}
})

  获取或这只内容的方法参数跟前两个有些变化,如果无参数则返回第一个元素的内容,此时亦不能链式调用,如果有参数,则将第一个参数作为内容来设置各个元素内容。

A.fn.extend({
	//获取或设置元素的内容
	html: function() {
		var arg = arguments,
			len = arg.length;
		//无参数则获取第一个元素的内容
		if(len === 0) {
			return this[0] && this[0].innerHTML;
		//一个参数则设置每个元素内容
		} else {
			for(var i = this.length - 1; i >= 0; i--) {
				this[i].innerHTML = arg[0];
			}
		}
		return this;
	}
})

  大功告成,不容易不容易,赶紧来测试一下我们自己的链式操作。

A('div')
.css({
	height: '30px',
	width: '40px'
})
.attr('class', 'demo')
.html('add demo text')
.on('click', function() {
	console.log('clicked');
});

  是不是很有趣,原理弄懂了,按照思路一步一步弄很容易。



<think>好的,我现在需要帮助用户让一个C++新手理解之前的代码。首先,我得回顾一下之前的对话,用户的问题是关于读取和输出双精度浮点数,保留12位小数。用户现在希望解释这段代码,让新手能明白。 首先,我要确定用户的需求是什么。他们可能是一个刚学C++的学生,或者刚开始接触编程的人,对语法和库函数不太熟悉。所以需要将代码分解,逐步解释每个部分的作用,使用简单易懂的语言,避免专业术语过多,或者至少解释清楚术语。 接下来,我需要回顾之前的代码代码包括头文件iostream和iomanip,使用命名空间std,主函数中读取一个double变量,然后使用cout配合fixed和setprecision输出。对于新手来说,每个部分都可能有问题,比如头文件的作用、using namespace std是什么、double是什么,输入输出流的使用,以及格式控制的含义。 然后,我应该分步骤解释代码,从整体结构开始,比如每个头文件的作用,为什么需要包含iomanip。然后解释主函数的结构,变量的声明,输入语句cin,再到输出的格式控制。重点在于解释fixed和setprecision的作用,因为这是保留12位小数的关键。同时,要指出为什么需要这些控制符,比如默认情况下cout可能会用科学计数法,而fixed确保小数点后的位数固定。 还要注意可能新手会犯的错误,比如忘记包含iomanip导致setprecision不可用,或者没有使用fixed导致输出格式不符合要求。需要强调这些细节的重要性。另外,代码中的endl的作用是什么,和"\n"的区别,是否需要解释?可能对新手来说,endl除了换行还有刷新缓冲区的功能,但这里可能暂时不需要深入,以免增加复杂度。 此外,代码示例中的注释是否足够清晰?可能需要更详细的注释,或者在解释时逐步拆解。例如,解释using namespace std的作用,避免新手不理解为什么可以直接用cin和cout而不用std::前缀。 可能还需要举例子,比如输入一个数,输出会是什么样子,特别是当小数位数不足12位时,是否会补零。例如,输入3.14,输出会是3.140000000000,这样新手可以直观地看到结果。 还要考虑新手可能对函数和库的理解不足,比如main函数的结构,return 0的意义,变量声明的位置等等。是否需要简要解释这些基础内容?可能用户已经知道这些,但为了确保全面性,还是应该提到。 最后,总结整个代码的执行流程,从输入到处理到输出,每一步的作用,帮助新手形成整体的认识。确保语言通俗易懂,避免过于技术性的描述,用比喻或实际例子辅助理解。</think>让我们用更简单的方式分解这段代码,帮助新手逐步理解: ### 代码详解(适合新手版) ```cpp // ① 引入工具包 #include <iostream> // 输入输出必备工具(cin/cout) #include <iomanip> // 格式控制工具(fixed/setprecision) // ② 使用标准工具库(避免写 std:: 前缀) using namespace std; // ③ 所有C++程序都必须有main函数,像程序的入口大门 int main() { // ④ 声明一个双精度浮点变量(能存带小数点的数) double num; // ⑤ 从键盘读取输入 >> 像水流进变量 cin >> num; // ⑥ 格式化输出(重点!) cout << fixed // 固定小数显示(不用科学计数法) << setprecision(12) // 显示12位小数(不够补零) << num // 输出数值 << endl; // 换行(相当于按下回车键) // ⑦ 告诉系统程序正常结束 return 0; } ``` ### 关键概念解释表 | 代码部分 | 作用说明 | 类比理解 | |-------------------|--------------------------------------------------------------------------|------------------------------| | `#include <...>` | 导入功能模块,就像玩游戏前要下载安装包 | 做菜前要准备好食材和厨具 | | `using namespace` | 使用标准工具库的缩写,避免每次都写`std::` | 说"手机"不用每次都加品牌前缀 | | `double num;` | 创建一个可以存储小数的容器(双精度浮点型),比`float`精度更高 | 准备一个高精度电子秤 | | `cin >> num` | 从键盘输入数值,存入`num`容器 | 把食材放进电子秤称重 | | `fixed` | 固定小数点显示模式(如不用这个,大数会显示成`1.23e+5`这样的科学计数法) | 让电子秤始终显示千克单位 | | `setprecision(12)`| 精确控制显示12位小数(第13位会四舍五入) | 设置电子秤显示到0.000000克 | | `cout << ...` | 把结果输出到屏幕 | 把称重结果打印到标签上 | ### 举个实际例子 🌰 **输入:** ```bash 3.141592653589793 ``` **输出:** ```bash 3.141592653590 ``` (注意:第13位是`8`,四舍五入后第12位`9`变成`10`,进位后显示为`90`) ### 新手常见疑问解答 ❓ 1. **为什么要用`double`不用`float`?** - `double`精度更高(15-17位有效数字),`float`只有6-9位,存12位小数必须用`double` 2. `fixed`和`setprecision`的顺序重要吗? - 非常重要!必须先写`fixed`,否则`setprecision`控制的是总有效位数(比如`cout << setprecision(3) << 12.345`会输出`12.3`) 3. **输入123会怎样显示?** - 输出`123.000000000000`,自动补足12位小数 4. **能处理超大数吗?** - 可以!`fixed`模式会完整显示,比如输入`123456789.123456789`会显示为`123456789.123456789000` 建议新手自己尝试修改代码: 1. 去掉`fixed`观察输出变化 2. 把12改成其他数字(如3或15) 3. 输入超过12位小数的数观察四舍五入效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值