前端基本功-示例代码 (二)
栏目: JavaScript · 发布时间: 5年前
内容简介:伪类 + transform 实现对于老项目,有没有什么办法能兼容1px的尴尬问题了,个人认为伪类+transform是比较完美的方法了。原理是把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。
1.一像素
伪类 + transform 实现
对于老项目,有没有什么办法能兼容1px的尴尬问题了,个人认为伪类+transform是比较完美的方法了。
原理是把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。
单条border样式设置:
.scale-1px{ position: relative; border:none; } .scale-1px:after{ content: ''; position: absolute; bottom: 0; background: #000; width: 100%; height: 1px; -webkit-transform: scaleY(0.5); transform: scaleY(0.5); -webkit-transform-origin: 0 0; transform-origin: 0 0; }
四条boder样式设置:
.scale-1px{ position: relative; margin-bottom: 20px; border:none; } .scale-1px:after{ content: ''; position: absolute; top: 0; left: 0; border: 1px solid #000; -webkit-box-sizing: border-box; box-sizing: border-box; width: 200%; height: 200%; -webkit-transform: scale(0.5); transform: scale(0.5); -webkit-transform-origin: left top; transform-origin: left top; }
最好在使用前也判断一下,结合 JS 代码,判断是否 Retina 屏:
if(window.devicePixelRatio && devicePixelRatio >= 2){ document.querySelector('ul').className = 'scale-1px'; }
方法二
/*移动端正常展示1px的问题 start*/ %border-1px{ display: block; position:absolute; left: 0; width: 100%; content: ' '; } .border-1px{ position: relative; &::after{ @extend %border-1px; bottom: 0; border-top: 1px solid #ccc; } &::before{ @extend %border-1px; top: 0; border-bottom: 1px solid #ccc; } } @media (-webkit-min-device-pixel-ratio:1.5),(min-device-pixel-ratio:1.5){ .border-1px{ &::after{ -webkit-transform: scaleY(0.7); transform: scaleY(0.7); } } } @media (-webkit-min-device-pixel-ratio:2),(min-device-pixel-ratio:2){ .border-1px{ &::after{ -webkit-transform: scaleY(0.5); transform: scaleY(0.5); } } } /*移动端正常展示1px的问题 end*/
方法三
.hairline-border { box-shadow: 0 0 0 1px; } @media (min-resolution: 2dppx) { .hairline-border { box-shadow: 0 0 0 0.5px red; } } @media (min-resolution: 3dppx) { .hairline-border { box-shadow: 0 0 0 0.33333333px; } } @media (min-resolution: 4dppx) { .hairline-border { box-shadow: 0 0 0 0.25px; } }
2.动画
animation:mymove 5s infinite; @keyframes mymove { from {top:0px;} to {top:200px;} }
js实现一个持续的动画效果
//兼容性处理 window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback){ window.setTimeout(callback, 1000 / 60); }; })(); var e = document.getElementById("e"); var flag = true; var left = 0; function render() { left == 0 ? flag = true : left == 100 ? flag = false : ''; flag ? e.style.left = ` ${left++}px` : e.style.left = ` ${left--}px`; } (function animloop() { render(); requestAnimFrame(animloop); })();
3. 实现sum(2)(3)
// 写一个 function 让下面两行代码输出的结果都为 5 console.log(sum(2, 3)); console.log(sum(2)(3));
实现
function sum() { var cache; if (arguments.length === 1) { cache = arguments[0]; return function ( number ) {return cache + number;}; } else return arguments[0] + arguments[1]; };
4. 函数柯里化
函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数或结果的新函数的技术。
函数柯里化的主要作用和特点就是参数复用、提前返回和延迟计算/执行。
1. 参数复用
引导
// 普通函数 function add(x,y){ return x + y; } add(3,4); //5 // 实现了柯里化的函数 // 接收参数,返回新函数,把参数传给新函数使用,最后求值 let add = function(x){ return function(y){ return x + y; } }; add(3)(4); // 7
通用的柯里化函数
感觉currying就是返回函数的函数,在此函数闭包中定义了私有域变量。
function curry(fn) { let slice = Array.prototype.slice, // 将slice缓存起来 args = slice.call(arguments, 1); // 这里将arguments转成数组并保存 return function() { // 将新旧的参数拼接起来 let newArgs = args.concat(slice.call(arguments)); return fn.apply(null, newArgs); // 返回执行的fn并传递最新的参数 } }
if (typeof Function.prototype.bind === "undefined"){ Function.prototype.bind = function (thisArgs){ var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function (){ let newArgs = args.concat(slice.call(arguments)) return fn.apply(thisArgs, newArgs); } } }
ES6版的柯里化函数
function curry(fn, ...allArgs) { const g = (...allArgs) => allArgs.length >= fn.length ? fn(...allArgs) : (...args) => g(...allArgs, ...args) return g; } // 测试用例 const foo = curry((a, b, c, d) => { console.log(a, b, c, d); }); foo(1)(2)(3)(4); // 1 2 3 4 const f = foo(1)(2)(3); f(5); // 1 2 3 5
function trueCurrying(fn, ...args) { if (args.length >= fn.length) { return fn(...args) } return function (...args2) { return trueCurrying(fn, ...args, ...args2) } } // 比较多次接受的参数总数与函数定义时的入参数量, //当接受参数的数量大于或等于被 Currying 函数的传入参数数量时, //就返回计算结果,否则返回一个继续接受参数的函数。 //注意这点和上边的区别
题目:需要写一个函数,满足
curry(fn)(1)(2)(3) //6
var fn = function(a,b,c) { return a+b+c; } function curry(fn) { var arr = [], mySlice = arr.slice fnLen = fn.length; function curring() { arr = arr.concat(mySlice.call(arguments)); if(arr.length < fnLen) { return curring; } return fn.apply(this, arr); } return curring; } curry(fn)(1)(2)(3);//6
本小题来自: 几个让我印象深刻的面试题(一)
2. 提前返回
var addEvent = function(el, type, fn, capture) { if (window.addEventListener) { el.addEventListener(type, function(e) { fn.call(el, e); }, capture); } else if (window.attachEvent) { el.attachEvent("on" + type, function(e) { fn.call(el, e); }); } };
上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,(eg. IE6/IE7)都会走一遍if...else if ...,其实只要一次判定就可以了,怎么做?–柯里化。改为下面这样子的代码:
var addEvent = (function(){ if (window.addEventListener) { return function(el, sType, fn, capture) { el.addEventListener(sType, function(e) { fn.call(el, e); }, (capture)); }; } else if (window.attachEvent) { return function(el, sType, fn, capture) { el.attachEvent("on" + sType, function(e) { fn.call(el, e); }); }; } })();
初始addEvent的执行其实值实现了部分的应用(只有一次的if...else if...判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。
对比:惰性加载
let addEvent = function(ele, type, fn) { if (window.addEventListener) { addEvent = function(ele, type, fn) { ele.addEventListener(type, fn, false); } } else if (window.attachEvent) { addEvent = function(ele, type, fn) { ele.attachEvent('on' + type, function() { fn.call(ele) }); } } addEvent(ele, type, fn);
3. 延迟计算/运行
ES5中的bind方法
if (!Function.prototype.bind) { Function.prototype.bind = function(context) { var self = this, args = Array.prototype.slice.call(arguments); return function() { return self.apply(context, args.slice(1)); } }; }
推荐阅读: 从一道面试题认识函数柯里化
参考文章: ES6版的柯里化函数 、 JS中的柯里化(currying)
5.手写一个 bind 方法
带一个参数:
Function.prototype.bind = function(context) { let self = this, slice = Array.prototype.slice, args = slice.call(arguments); return function() { return self.apply(context, args.slice(1)); } };
带多个参数:
//ES3实现 if(!Function.prototype.bind){ Function.prototype.bind = function(o, args){ var self = this, boundArgs = arguments;//注:arguments是指sum.bind(null,1)中的参数null和1 return function(){ //此时返回的只是一个函数 var args = [], i; for(var i=1; i< boundArgs.length; i++){ args.push(boundArgs[i]); } for(var i =0; i< arguments.length; i++){ args.push(arguments[i]);//注:这里的arguments是指result(2)中的参数2 } return self.apply(o, args); } } }
或者
// 代码来自书籍 《javaScript 模式》 if (typeof Function.prototype.bind === "undefined"){ Function.prototype.bind = function (thisArgs){ var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function (){ return fn.apply(thisArgs, args.concat(slice.call(arguments))); } } } //注:前后arguments不是一回事哦~ //调用 var sum = function(x,y){ return x+y }; var result = sum.bind(null,1); result(2); // 3
或者
Function.prototype.bind = function(){ var fn = this; var args = Array.prototye.slice.call(arguments); var context = args.shift(); return function(){ return fn.apply(context, args.concat(Array.prototype.slice.call(arguments))); };
本节参考文章:js中的bind
其他文章: JavaScirpt 的 bind 函数究竟做了哪些事
6.经典面试问题:new 的过程
首先来看一下,函数声明的过程
// 实际代码 function fn1() {} // JavaScript 自动执行 fn1.protptype = { constructor: fn1, __proto__: Object.prototype } fn1.__proto__ = Function.prototype
var a = new myFunction("Li","Cherry"); //伪代码 new myFunction{ var obj = {}; obj.__proto__ = myFunction.prototype; var result = myFunction.call(obj,"Li","Cherry"); return typeof result === 'object'? result : obj; }
- 创建一个空对象 obj;
- 将新创建的空对象的隐式原型指向其构造函数的显示原型。
- 使用 call 改变 this 的指向
- 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
所以我们可以看到,在 new 的过程中,我们是使用 call 改变了 this 的指向。
7.javascript里面的继承怎么实现,如何避免原型链上面的对象共享
什么是原型链
当一个引用类型继承另一个引用类型的属性和方法时候就会产生一个原型连。
ES5:寄生组合式继承:通过借用构造函数来继承属性和原型链来实现子继承父。
function ParentClass(name) { this.name = name; } ParentClass.prototype.sayHello = function () { console.log("I'm parent!" + this.name); } function SubClass(name, age) { //若是要多个参数可以用apply 结合 ...解构 ParentClass.call(this, name); this.age = age; } SubClass.prototype.sayChildHello = function (name) { console.log("I'm child " + this.name) } SubClass.prototype = Object.create(ParentClass.prototype); SubClass.prototype.constructor = SubClass; let testA = new SubClass('CRPER') // Object.create()的polyfill /* function pureObject(obj){ //定义了一个临时构造函数 function F() {} //将这个临时构造函数的原型指向了传入进来的对象。 F.prototype = obj; //返回这个构造函数的一个实例。该实例拥有obj的所有属性和方法。 //因为该实例的原型是obj对象。 return new F(); } */ 或 function subClass() { superClass.apply(this, arguments); this.abc = 1; } function inherits(subClass, superClass) { function Inner() {} Inner.prototype = superClass.prototype; subClass.prototype = new Inner(); subClass.prototype.constructor = subClass; } inherits(subClass, superClass); subClass.prototype.getTest = function() { console.log("hello") };
ES6: 其实就是ES5的语法糖,不过可读性很强..
class ParentClass { constructor(name) { this.name = name; } sayHello() { console.log("I'm parent!" + this.name); } } class SubClass extends ParentClass { constructor(name) { super(name); } sayChildHello() { console.log("I'm child " + this.name) } // 重新声明父类同名方法会覆写,ES5的话就是直接操作自己的原型链上 sayHello(){ console.log("override parent method !,I'm sayHello Method") } } let testA = new SubClass('CRPER')
8.继承 JS 内置对象(Date)
写在前面,本节只记录了 如何继承Date对象... 的解决方案,具体问题和解析过程请看原文
ES5
// 需要考虑polyfill情况 Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) { obj.__proto__ = proto; return obj; }; /** * 用了点技巧的继承,实际上返回的是Date对象 */ function MyDate() { // bind属于Function.prototype,接收的参数是:object, param1, params2... var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))(); // 更改原型指向,否则无法调用MyDate原型上的方法 // ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__ Object.setPrototypeOf(dateInst, MyDate.prototype); dateInst.abc = 1; return dateInst; } // 原型重新指回Date,否则根本无法算是继承 Object.setPrototypeOf(MyDate.prototype, Date.prototype); MyDate.prototype.getTest = function getTest() { return this.getTime(); }; let date = new MyDate(); // 正常输出,譬如1515638988725 console.log(date.getTest());
ES6
class MyDate extends Date { constructor() { super(); this.abc = 1; } getTest() { return this.getTime(); } } let date = new MyDate(); // 正常输出,譬如1515638988725 console.log(date.getTest());
注意:这里的正常输出环境是直接用ES6运行,不经过babel打包,打包后实质上是转化成ES5的,所以效果完全不一样,会报错的
9.简易双向数据绑定
<body> <input type="text" id="foo"> <p id="test"></p> <script> var user = {} Object.defineProperty(user, 'inputValue', { configurable: true, get: function() { return document.getElementById('foo').value }, set: function(value) { document.getElementById('foo').value = value document.getElementById('test').innerHTML = value } }) document.getElementById('foo').addEventListener('keyup', function() { document.getElementById('test').innerHTML = user.inputValue }) </script> </body>
10.JavaScript实现发布-订阅模式
发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。JavaScript开发中我们一般用事件模型来代替传统的发布-订阅模式
示例1
function Dep() {//发布者 this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { this.subs.forEach(sub=>sub.update()); } function Watcher(fn) {//订阅者 this.fn = fn; } Watcher.prototype.update = function () { this.fn(); } var dep = new Dep(); dep.addSub(new Watcher(function () { console.log('okokok'); })) dep.notify();
推荐阅读: Javascript设计模式之发布-订阅模式
示例2
function Event(){ this.list={}, this.on=function(key,cb){//订阅事件 if(!this.list[key]){ this.list[key] = [] } this.list[key].push(cb) }, this.emit = function(){//触发事件 var key = Array.prototype.shift.call(arguments) var e = this.list[key] if(!e){ return } var args = Array.prototype.slice.call(arguments) for(var i = 0;i<e.length;i++){ e[i].apply(null,args) } } }
尝试一下:
var a = new Event() a.on('a',function(x){console.log(x)}) a.emit('a',1)//1
推荐阅读: 从单向到双向数据绑定
示例3
var myBus = (function() { var clienlist = {}, addlisten, trigger, remove; /** * 增加订阅者 * @key {String} 类型 * @fn {Function} 回掉函数 * */ addlisten = function(key, fn) { if(!clienlist[key]) { clienlist[key] = []; } clienlist[key].push(fn); }; /** * 发布消息 * */ trigger = function() { var key = [].shift.call(arguments), //取出消息类型 fns = clienlist[key]; //取出该类型的对应的消息集合 if(!fns || fns.length === 0) { return false; } for(var i = 0, fn; fn = fns[i++];) { fn.apply(this, arguments); } }; /** * 删除订阅 * @key {String} 类型 * @fn {Function} 回掉函数 * */ remove = function(key, fn) { var fns = clienlist[key]; //取出该类型的对应的消息集合 if(!fns) { //如果对应的key没有订阅直接返回 return false; } if(!fn) { //如果没有传入具体的回掉,则表示需要取消所有订阅 fns && (fns.length = 0); } else { for(var l = fns.length - 1; l >= 0; l--) { //遍历回掉函数列表 if(fn === fns[l]) { fns.splice(l, 1); //删除订阅者的回掉 } } } }; return { $on: addlisten, $emit: trigger, $off: remove } })();
推荐阅读: 写一个简单vue 中间件,$emit、$on
示例4
这个示例更像 示例2 、 示例3 的总结,我也放这里吧,多看几种写法也多少开阔一下思路或全当复习
卖烧饼的店主可以把小明、小龙的电话记录下来,等店里有烧饼了在通知小龙小明来拿这就是所谓的发布-订阅模式,代码如下:
/*烧饼店*/ var Sesamecakeshop={ clienlist:[],//缓存列表 addlisten:function(fn){//增加订阅者 this.clienlist.push(fn); }, trigger:function(){//发布消息 for(var i=0,fn;fn=this.clienlist[i++];){ fn.apply(this,arguments); } } } /*小明发布订阅*/ Sesamecakeshop.addlisten(function(price,taste){ console.log("小明发布的"+price+"元,"+taste+"味道的"); }); /*小龙发布订阅*/ Sesamecakeshop.addlisten(function(price,taste){ console.log("小龙发布的"+price+"元,"+taste+"味道的"); }); Sesamecakeshop.trigger(10,"椒盐");
从代码中可以看出,只有小明,小龙预定了烧饼,烧饼店就可以发布消息告诉小龙与小明。但是有个问题不知道大家发现了没有。小明只喜欢椒盐味道的。而小龙只喜欢焦糖味道的。上面的代码就满足不了客户的需求,给客户一种感觉就是,不管我喜欢不喜欢,你都会发给我。如果发布比较多,客户就会感到厌烦,甚至会想删除订阅。下边是对代码进行改良大家可以看看。
/*烧饼店*/ var Sesamecakeshop={ clienlist:{},/*缓存列表*/ /** * 增加订阅者 * @key {String} 类型 * @fn {Function} 回掉函数 * */ addlisten:function(key,fn){ if(!this.clienlist[key]){ this.clienlist[key]=[]; } this.clienlist[key].push(fn); }, /** * 发布消息 * */ trigger:function(){ var key=[].shift.call(arguments),//取出消息类型 fns=this.clienlist[key];//取出该类型的对应的消息集合 if(!fns || fns.length===0){ return false; } for(var i=0,fn;fn=fns[i++];){ fn.apply(this,arguments); } }, /** * 删除订阅 * @key {String} 类型 * @fn {Function} 回掉函数 * */ remove:function(key,fn){ var fns=this.clienlist[key];//取出该类型的对应的消息集合 if(!fns){//如果对应的key没有订阅直接返回 return false; } if(!fn){//如果没有传入具体的回掉,则表示需要取消所有订阅 fns && (fns.length=0); }else{ for(var l=fns.length-1;l>=0;l--){//遍历回掉函数列表 if(fn===fns[l]){ fns.splice(l,1);//删除订阅者的回掉 } } } } } /*小明发布订阅*/ Sesamecakeshop.addlisten("焦糖",fn1=function(price,taste){ console.log("小明发布的"+price+"元,"+taste+"味道的"); }); /*小龙发布订阅*/ Sesamecakeshop.addlisten("椒盐",function(price,taste){ console.log("小龙发布的"+price+"元,"+taste+"味道的"); }); Sesamecakeshop.trigger("椒盐",10,"椒盐"); Sesamecakeshop.remove("焦糖",fn1);//注意这里是按照地址引用的。如果传入匿名函数则删除不了 Sesamecakeshop.trigger("焦糖",40,"焦糖");
推荐必读: 发布-订阅模式
11.扁平化后的数组
如:[1, [2, [ [3, 4], 5], 6]] => [1, 2, 3, 4, 5, 6]
var data = [1, [2, [ [3, 4], 5], 6]]; function flat(data, result) { var i, d, len; for (i = 0, len = data.length; i < len; ++i) { d = data[i]; if (typeof d === 'number') { result.push(d); } else { flat(d, result); } } } var result = []; flat(data, result); console.log(result);
12.冒泡排序
解析:
- 比较相邻的两个元素,如果前一个比后一个大,则交换位置。
- 第一轮的时候最后一个元素应该是最大的一个。
- 按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。
js代码实现
function bubble_sort(arr){ for(var i = 0;i < arr.length - 1; i++){ for(var j = 0;j < arr.length - i - 1;j++){ if(arr[j] > arr[j+1]){ [arr[j], arr[j+1]] = [arr[j + 1], arr[j]] } } } } var arr = [3,1,5,7,2,4,9,6,10,8]; bubble_sort(arr); console.log(arr);
13.快速排序
快速排序是对冒泡 排序 的一种改进
解析:
- 第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。
- 然后递归调用,在两边都实行快速排序。
js代码实现
function quick_sort(arr){ if(arr.length <= 1){ return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0;i < arr.length; i++) { if(arr[i] < pivot){ left.push(arr[i]); } else { right.push(arr[i]); } } return quick_sort(left).concat([pivot],quick_sort(right)); } var arr=[5,6,2,1,3,8,7,1,2,3,4,7]; console.log(quick_sort(arr));
14.选择排序
// 选择排序:大概思路是找到最小的放在第一位,找到第二小的放在第二位,以此类推 算法复杂度O(n^2) 选择demo: function selectionSort(arr) { let len = arr.length; let minIndex; for (let i = 0; i < len - 1; i++) { minIndex = i; for (let j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { //寻找最小的数 minIndex = j; //将最小数的索引保存 } } [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; } return arr; }
本节参考文章: 2018前端面试总结...
15.插入排序
解析:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到下一位置中
- 重复步骤2
js代码实现
function insert_sort(arr){ var i=1, j,key,len=arr.length; for(;i<len;i++){ var j=i; var key=arr[j]; while(--j>-1){ if(arr[j]>key){ arr[j+1]=arr[j]; }else{ break; } } arr[j+1]=key; } return arr; } 或 function insert_sort(arr) { let len = arr.length; let preIndex, current; for (let i = 1; i < len; i++) { preIndex = i - 1; current = arr[i]; while (preIndex >= 0 && arr[preIndex] > current) { arr[preIndex + 1] = arr[preIndex]; preIndex--; } arr[preIndex + 1] = current; } return arr; } insert_sort([2,34,54,2,5,1,7]);
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 前端基本功-常见概念(二)
- 前端基本功-示例代码(一)
- 前端基本功-常见概念(一)
- 前端基本功-响应式布局(flex)
- 前端基本功(七):javascript中的继承(原型、原型链、继承的实现方式)
- 前端基本功:JavaScript 的七种数据类型与数据类型检测的4种方法
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
啊哈C语言!逻辑的挑战(修订版)
啊哈磊 / 电子工业出版社 / 2017-1 / 49
《啊哈C语言!逻辑的挑战(修订版)》是一本非常有趣的编程启蒙书,《啊哈C语言!逻辑的挑战(修订版)》从中小学生的角度来讲述,没有生涩的内容,取而代之的是生动活泼的漫画和风趣幽默的文字。配合超萌的编程软件,《啊哈C语言!逻辑的挑战(修订版)》从开始学习与计算机对话到自己独立制作一个游戏,由浅入深地讲述编程的思维。同时,与计算机展开的逻辑较量一定会让你觉得很有意思。你可以在茶余饭后阅读《啊哈C语言!逻......一起来看看 《啊哈C语言!逻辑的挑战(修订版)》 这本书的介绍吧!