共计 17188 个字符,预计需要花费 43 分钟才能阅读完成。
前言
2018/04/27 新增六,讲解浅拷贝和深拷贝的区别并简单实现, 七,原生 JS 操作 DOM?
2018/04/30 新增八,解决计算精度问题, 例如 0.1+0.2?
2018/05/06 修改代码格式
2018/11/06 新增一个遇到的闭包执行环境的面试题
一道常被人轻视的前端 JS 面试题
作者:小小沧海
出处:http://www.cnblogs.com/xxcanghai/
本文地址:http://www.cnblogs.com/xxcanghai/
(题型是一样的,衹是用我自己的理解去分析,为了不混淆,我略微修改下代码而已。说来惭愧,虽然我只错了一题,最难的后面三题都对了,但是思路是错的,所以希望大家不要衹看答案,重点是学习其中的原理)
function Person() {getAge = function() {console.log(10);
};
return this;
}
Person.getAge = function() {console.log(20);
};
Person.prototype.getAge = function() {console.log(30);
};
var getAge = function() {console.log(40);
};
function getAge() {console.log(50);
}
Person.getAge();
getAge();
Person().getAge();
getAge();
new Person.getAge();
new Person().getAge();
new new Person().getAge();
(原谅我没有效果代码给你们看, 因为不知道什么原因无法在编辑器里输出, 你们就放到本地打印看看好了┑(~Д ~)┍)
这是一道涉及知识点超多的题目, 包括函数声明, 变量提升,this 指向,new 新对象, 优先级, 原型链,继承,对象属性和原型属性等等.
(答案就不贴出来了, 你们可以自己跑一下, 怕你们忍不住先看正确答案.)
首先分析下上面都做了些什么。
- 定义一个 Person 函数,里面有一个 getAge 的匿名函数
- 为 Person 函数本身定义一个静态属性 getAge 函数
- 为 Person 函数原型上定义一个 getAge 函数
- 变量声明一个 getAge 函数表达式
- 直接声明一个 getAge 函数
Person.getAge();
拆分开来看
function Person() {getAge = function() {console.log(10);
};
return this;
}
console.log(Person); // 看看 Person 是什么
Person.getAge = function() {console.log(20);
};
Person.getAge();
很明显是直接调用 Person 的静态属性 getAge,结果就是 20 了。(详情可以参考我之前写的文章关于 Javascript 中的 new 运算符, 继承与原型链一些理解)
getAge();
首先前面不带对象,所以可以知道是全局环境调用不用考虑 Person 的部分
这题考察的是函数声明和函数表达式
getAge(); //50
var getAge = function() {console.log(40);
};
getAge(); //40
function getAge() {console.log(50);
}
getAge(); //40
上面可以看到首先 getAge 指向函数声明,直到函数表达式那一步之后才被覆盖。
这就要理解 Javascript Function 两种类型的区别:用函数声明创建的函数可以在函数解析后调用(解析时进行等逻辑处理);而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用。
变量对象的创建,依次经历了以下几个过程。
- 建立 arguments 对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
- 检查当前上下文的函数声明,也就是使用 function 关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
- 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为 undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为 undefined,则会直接跳过,原属性值不会被修改。
所以这一步实际运行顺序如下
function getAge() {
// 函数提升解析赋值给 getAge
console.log(50);
}
var getAge; // 变量提升,此时 getAge 为 undefined
getAge(); //50,此时还是函数声明
// 表达式覆盖变量赋值
getAge = function() {console.log(40);
};
getAge(); //40
getAge(); //40
Person().getAge();
- 前面直接执行 Person(), 返回一个对象指向全局 window, 所以不用考虑第二步函数本身定义静态属性, 也就是 window.getAge(), 根据上一答知道函数声明会被覆盖, 也不会进入到原型链搜索 getAge 函数, 所以能排除第三步和第五步的干扰.
- window.getAge(), 这里的关键也是一个陷阱在于 函数内部的 getAge 函数赋值是没带声明!!!
区别就在: 如果带有函数声明, 当执行 Person(), 因为这是属于 Person 函数的局部变量 getAge 函数, 外部调用的全局变量的 getAge 函数, 所以输出的自然是 40
function Person() {var getAge = function() {console.log(10);
};
return this;
}
var getAge = function() {console.log(40);
};
Person().getAge();
如果不带有函数声明, 当执行 Person()因为这时属于 Person 函数的 getAge 函数赋值覆盖外部全局变量的 getAge 函数, 所以输出的自然是 10
function Person() {getAge = function() {console.log(10);
};
return this;
}
var getAge = function() {console.log(40);
};
Person().getAge();
getAge();
这一步主要受上面影响, 也是进一步论证上一步的说法, 因为全局变量的 getAge 函数已经被覆盖了, 所以现在直接调用全局 getAge 输出的就是 10.
new Person.getAge();
这里的难点在于符号优先级
(截图来自运算符优先级)
一般人认为是这样子的
(new Person).getAge(); // 10
从截图可以知道
成员访问(点符号)= new(带参数列表) > 函数调用 = new (无参数列表)
其实当时这一步看了优先级之后从那角度去理解我就迷糊, 如果优先级为准, 怎么运行都会报错的, 先运行点符号, 然后 new 没带参数应该函数调用优先, 最后才到 new 那一步
new ((Person.getAge)()); //Uncaught TypeError: Person.getAge(...) is not a constructor
后来看到评论说了才明白一些,我理解错了一些地方。
首先带参数的 new 不是说必须传参才算,而是后面带了括号就算是带参了。
new Person()// 带参
new Person// 没带参
实际上是这样子的,从整体去了解
new (Person.getAge)();//20
因为不带参数的 new 优先级不如成员访问 (点符号),所以首先执行 Person.getAge.
然后 new(带参数列表)优先级高于函数调用,所以将 Person.getAge 函数作为了构造函数来执行如 new xxx()实例化一个东西出来
最好先弄清这一题的来龙去脉,然后才能解决后面更加绕的问题。
new Person().getAge();
理解了运算符优先级问题之后下面其实都好做
先来一步步剖析,
- 题目里成员访问 (点符号) 和 new(带参数列表)最优先, 同优先级情况下从左往右计算,所以先执行 new Person();
- 因为 Person 函数内的 getAge 只是一个赋值函数, 所以所有实例都没有继承这个函数, 只能从原型链上寻找到 getAge 函数,(输出 30 那个)
(new Person()).getAge();//30
new new Person().getAge();
承接上面步骤继续一步步剖析,
- 题目里成员访问 (点符号) 和 new(带参数列表)最优先, 同优先级情况下从左往右计算,所以先执行 new Person();
- 因为 Person 函数内的 getAge 只是一个赋值函数, 所以所有实例都没有继承这个函数, 只能从原型链上寻找到 getAge 函数,(输出 30 那个),所以先执行 new Person().getAge;
- 将 new Person().getAge 函数作为了构造函数来执行如 new xxx()实例化一个东西出来,执行 new ((new Person()).getAge)(),结果还是输出 30
大概意思就这样了,不知道我讲清楚了没有?
二,关于强制转换类型
(这是我在研究隐形转换的时候折腾出来的问题, 里面弯弯绕绕挺多的, 可足以坑死很多人, 看看你们能不能做全对)
console.log(Number(null));
console.log(Number(undefined));
console.log(Number({}));
console.log(Number({abc: 123}));
console.log(undefined == null);
console.log(NaN == null);
console.log(null == null);
console.log(NaN == NaN);
console.log([1] == true);
console.log([[1], [2], [3]] == '1,2,3');
console.log('' == false);
console.log(null == false);
console.log({} == false);
console.log({} == []);
console.log([] == false);
console.log([] == []);
console.log(![] == false);
console.log(![] == []);
console.log(new Boolean(true) == 1);
console.log(new Boolean(false) == 0);
console.log(new Boolean(true) ? true : false);
console.log(new Boolean(false) ? true : false);
这是一道涉及知识点不算多, 但是很能体现 javascript 语言的奇形怪状, 其实所有的规律我都已经写出来过了, 这些题型也是从里面想出来的,(详情可以参考我之前写的文章 javascript 中关于相等符号的隐形转换)
先看看关于 Number 的问题,
- 如果是 null 值,返回 0。
- 如果是 undefined,返回 NaN。
- {}先调用对象的 valueOf()方法还是 {}, 再调用 toString() 方法输出 ”[object Object]”, 得出字符串再调用 Number()因为无效字符串返回 NaN
- 同上
console.log(Number(null)); //0
console.log(Number(undefined)); //NaN
console.log(Number({})); //NaN
console.log(Number({abc: 123})); //NaN
接着是关于 undefined,NaN ,null 之间的不完全相等关系,
null 和 undefined 是相等的,undefined 和 undefined 是相等的,null 和 null 也是相等的,
但是如果有一个操作数是 NaN 则相等操作符返回 false,而不相等操作符返回 true。(即使两个操作数都是 NaN,相等操作符也返回 false 因为按照规则 NaN 不等于 NaN。)
console.log(undefined == null); //true
console.log(NaN == null); //false
console.log(null == null); //true
console.log(NaN == NaN); //false
下面关于转换类型问题
1, 如果一个操作数是布尔值. 则在比较相等性之前先将其转换为数值 false 转换为 0,而 true 转换为 1;
2, 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串调用 Number() 转换为数值;
3, 如果一个操作数是对象,另一个操作数不是,则先调用对象的 valueOf, 再调用对象的 toString 与基本类型进行比较。也就是说先转成 number 型,不满足就继续再转成 string 类型按照前面的规则进行比较;
- [1]先调用对象的 valueOf()方法还是 [1], 再调用 toString() 方法输出 ”1″, 得出字符串再调用 Number()返回 1,
true 调用 Number()直接返回 1 - [[1],[2],[3]]先调用对象的 valueOf()方法还是 [[1],[2],[3]], 再调用 toString() 方法输出 ”1,2,3″ 直接比较
(关于 toString(), 如果是 Array 值,将 Array 的每个元素转换为字符串,并用逗号作为分隔符进行拼接。)
console.log([1] == true); //true
console.log([[1], [2], [3]] == '1,2,3'); //true
数据类型跟布尔值比较, 回顾下前面说的要点, 然后有几个应该要知道的隐形转换:
null 和 undefined 不能转换成其他任何值。
false -> 0.
[] -> 0.
{} -> NaN.
然后可以做出大部分题型了
- 0==0
- null==0
- NaN==0
- 迷惑题, 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true; 否则返回 false(容易固定思维转化比较, 尽管结果也对, 但是不会转换类型比较的)
- 0==0
- 迷惑题, 虽然都长得一样, 但是引用类型指向地址不同就不会相等(详情可以参考我之前写的文章关于 javascript 基本类型和引用类型小知识)
-
迷惑题, 加了取反符号之后就要考虑情况更多了,
首先根据上一题答案里有讲解过符号优先级的问题,! 优先级高于 ==, 所以前面是一个判断,[] == true, 取反就是 false 了,(![] ? true : false) == [] -> 0 == 0
这题主要难点在于考虑符号优先级的问题, 先判断再比较.
- 同 7
console.log('' == false); //true
console.log(null == false); //false
console.log({} == false); //false
console.log({} == []); //false
console.log([] == false); //true
console.log([] == []); //false
console.log(![] == false); //true
console.log(![] == []); //true
最后跟构造函数有关, 容易被误导 (详情可以参考我之前写的文章关于 Javascript 中的构造函数, 原型链与 new 运算符一些理解)
构造函数创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一, 所以 new Boolean 返回的不是布尔值, 而是内置对象类型, 详情如下
console.log(typeof Boolean(true));
console.log(typeof new Boolean(true));
console.log(typeof new Boolean(true).valueOf());
console.log(typeof new Boolean(true).toString());
所以知道结果如下:
- new Boolean(true)先调用对象的 valueOf()方法返回 true, 再调用 toString()方法输出 ”true”, 得出字符串再调用 Number()返回 1
- 原理如上
- 3 和 4, 这里不是比较, 而是 if 判断, 因为存在对象, 所以都是返回 true
console.log(new Boolean(true) == 1); //true
console.log(new Boolean(false) == 0); //true
console.log(new Boolean(true) ? true : false); //true
console.log(new Boolean(false) ? true : false); //true
三,关于 z -index 层级树
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style media="screen">
.one {
position: relative;
z-index: 2;
}
.two {z-index: 3;}
.three {
position: absolute;
top: 0;
z-index: 2;
}
.four {
position: absolute;
z-index: -1;
}
.six {
position: absolute;
top: 0;
z-index: 5;
}
</style>
</head>
<body>
<div class="one">
<div class="two"></div>
<div class="three"></div>
</div>
<div class="four">
<div class="five"></div>
<div class="six"></div>
</div>
</body>
</html>
依据结构样式, 给出例子里的层级先后顺序, 这里不太好说, 大家直接看看原理再试一次看看吧
css 中 z -index 层级
CSS z-index 属性的使用方法和层级树的概念
CSS 基础(七):z-index 详解
深入理解 CSS 定位中的堆叠 z -index
我给一个颜色版给大家做参考
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style media="screen">
.one, .four{width: 400px; height: 100px;}
.two, .five{width: 200px; height: 100px;}
.three, .six{width: 100px; height: 100px;}
.one{border:5px solid red}
.two{background-color: green}
.three{background-color: blue}
.four{border:5px solid pink; margin-top: -20px;}
.five{background-color: orange}
.six{background-color: black}
.one{position: relative; z-index: 2}
.two{z-index: 3}
.three{position: absolute; top: 0; z-index: 2}
.four{position: absolute; z-index: -1}
.six{position: absolute; top: 0; z-index: 5}
</style>
</head>
<body>
<div class="one">
<div class="two"></div>
<div class="three"></div>
</div>
<div class="four">
<div class="five"></div>
<div class="six"></div>
</div>
</body>
</html>
四,关于深入理解 JS 中的 Function.prototype.bind()方法原理 & 兼容写法
一般来说我们想到改变函数 this 指向的方法无非就 call、apply 和 bind;
方法名 | 描述 |
---|---|
call | 调用一个对象的一个方法,以另一个对象替换当前对象, 余参按顺序传递 |
apply | 调用一个对象的一个方法,以另一个对象替换当前对象, 余参按数组传递 |
bind | 创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数 |
他们之间的区别在于调用时机或者添加参数形式不同, 所以我们可以用里面的方法做兼容.
Prototype.js 中的写法
Function.prototype.bind = function() {
var fn = this,
args = [].prototype.slice.call(arguments),
object = args.shift();
return function() {return fn.apply(object, args.concat([].prototype.slice.call(arguments)));
};
};
Firefox 为 bind 提供了一个兼容实现
if (!Function.prototype.bind) {Function.prototype.bind = function(oThis) {if (typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(
this instanceof fNOP && oThis ? this : oThis || window,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
五,前端经典面试题: 从输入 URL 到页面加载发生了什么?
亲身遇到面试题之一,知识点太多了,赠你飞机票前端经典面试题: 从输入 URL 到页面加载发生了什么?
六,讲解浅拷贝和深拷贝的区别并简单实现
简单直观点说:
- 浅拷贝只会将原对象的各个属性进行依次复制,而 JavaScript 存储对象都是存地址的,所以浅复制会导致深层对象属性都指向同一块内存地址;
- 深拷贝不仅将原对象的各个属性进行依次复制,而且将原对象的深层对象属性也依次采用深拷贝的方法递归复制到新对象上;
(详情可以参考我之前写的文章关于 javascript 基本类型和引用类型小知识)
浅拷贝简单实现:
function shallowCopy(obj) {var _obj = {},
key;
// 如果使用 Object.keys 更方便
for (key in obj) {
// 只复制对象本身首层属性
if (obj.hasOwnProperty(key)) {_obj[key] = obj[key];
}
}
return _obj;
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = shallowCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
浅拷贝 ES5 实现:
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象
function shallowCopy(obj) {return Object.assign({}, obj);
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = shallowCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
深拷贝简单实现:
function deepCopy(obj) {var _obj = {},
key;
// 如果使用 Object.keys 更方便
for (key in obj) {if (obj.hasOwnProperty(key)) {if (typeof obj[key] === 'object') {_obj[key] = deepCopy(obj[key]);
} else {_obj[key] = obj[key];
}
}
}
return _obj;
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = deepCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
注意:
- for in 遍历性能太差, 会包括对象原型链上的属性, 即使加上 hasOwnProperty 判断也只是减少操作属性;
- 有人会在递归使用 arguments.callee 方法, 但是 ECMAscript5 的 Strict Mode 是禁止使用的, 因为 arguments 是庞大且变化的 Why was the arguments.callee.caller property deprecated in JavaScript?
函数名调用好处:
- 这个函数可以像其他任何代码一样在代码中调用。
- 它不会污染名称空间。
- 它的值不会改变。
- 它性能更好(访问参数对象是昂贵的)。
深拷贝转格式写法:
序列化成 JSON 字符串的值会新开一个存储地址,从而分开两者关联;
function deepCopy(obj) {return JSON.parse(JSON.stringify(obj));
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = deepCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
注意:
- 只能处理能够被 json 直接表示的数据结构(Number, String, Boolean, Array, 扁平对象等), 不支持 NaN,Infinity,循环引用和 function 等;
- 如果构造实例, 会切断原有对象的 constructor 等相关属性;
深拷贝 Object.create 写法:
创建一个具有指定原型且可选择性地包含指定属性的对象
function deepCopy(obj) {var _obj = {},
key;
// 如果使用 Object.keys 更方便
for (key in obj) {if (obj.hasOwnProperty(key)) {if (typeof obj[key] === 'object') {_obj[key] = Object.create(obj[key]);
} else {_obj[key] = obj[key];
}
}
}
return _obj;
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = deepCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
注意:
属性不在复制对象本身属性中, 而在原型链上;
(详情可以参考我之前写的文章关于创建对象的三种写法 —- 字面量,new 构造器和 Object.create())
不考虑兼容问题的话可以这么写:
function deepCopy(obj) {var _obj = obj.constructor === Array ? [] : {};
if (window.JSON) {_obj = JSON.parse(JSON.stringify(obj));
} else {Object.keys(obj).map(key => {_obj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
});
}
return _obj;
}
var obj1 = {
a: 1,
b: {c: 2,},
},
obj2;
// 复制对象并改变原对象值
obj2 = deepCopy(obj1);
obj1.a = 2;
obj1.b.c = 3;
console.log(obj1, obj2);
七,原生 JS 操作 DOM?
说多都是泪, 从原生到 JQ 到 MV* 框架, 老祖宗都模糊了.
查找节点
方法 | 作用 |
---|---|
document.getElementById | 根据 ID 查找元素,大小写敏感,如果有多个结果,只返回第一个 |
document.getElementsByClassName | 根据类名查找元素,多个类名用空格分隔,返回一个 HTMLCollection。注意兼容性为 IE9+(含)。另外,不仅仅是 document,其它元素也支持 getElementsByClassName 方法 |
document.getElementsByTagName | 根据标签查找元素,* 表示查询所有标签,返回一个 HTMLCollection |
document.getElementsByName | 根据元素的 name 属性查找,返回一个 NodeList |
document.querySelector | 指定一个或多个匹配元素的 CSS 选择器。可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。返回单个 Node,IE8+(含),如果匹配到多个结果,只返回第一个 |
document.querySelectorAll | 指定一个或多个匹配元素的 CSS 选择器。可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。返回一个 NodeList,IE8+(含) |
document.forms | 获取当前页面所有 form,返回一个 HTMLCollection |
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<ul id="id">
<li class="list1">
<input type="text" name="name" value="">
</li>
<li class="list1"></li>
<li class="list1"></li>
<li class="list1 list2"></li>
<li class="list1 list2"></li>
</ul>
<script type="text/javascript">
console.log(document.getElementById('id'));
console.log(document.getElementsByClassName('list1'));
console.log(document.getElementsByClassName('list1 list2'));
console.log(document.getElementsByTagName('*'));
console.log(document.getElementsByTagName('li'));
console.log(document.getElementsByName('name'));
console.log(document.querySelector('#id'));
console.log(document.querySelector('.list1'));
console.log(document.querySelectorAll('.list1'));
</script>
</body>
</html>
创建节点
方法 | 作用 |
---|---|
document.createElement | 创建元素 |
document.createTextNode | 创建文本 |
document.cloneNode | 克隆元素, 接收一个 bool 参数,用来表示是否复制子元素。 |
document.createDocumentFragment | 创建文档碎片 |
document.createComment | 创建注释节点 |
// 创建并插入文本
var ele = document.createElement('div'),
txt = document.createTextNode('123'),
cmt = document.createComment('comments');
ele.appendChild(txt);
ele.appendChild(cmt);
// 克隆元素
var clone_ele1 = ele.cloneNode(),
clone_ele2 = ele.cloneNode(true);
console.log(ele);
console.log(txt);
console.log(clone_ele1);
console.log(clone_ele2);
修改节点
方法 | 作用 |
---|---|
parent.appendChild(child) | 将 child 追加到 parent 的子节点的最后面 |
parentNode.insertBefore(newNode, refNode) | 将某个节点插入到另外一个节点的前面 |
parent.removeChild(child) | 删除指定的子节点并返回子节点 |
parent.replaceChild(child) | 将一个节点替换另一个节点 |
parent.insertData(child) | 将数据插入已有的文本节点中 |
// 创建并插入文本
var ele = document.createElement('div'),
txt = document.createTextNode('123'),
txt2 = document.createTextNode('456'),
cmt = document.createComment('comments');
ele.appendChild(txt);
ele.insertBefore(cmt, txt);
ele.removeChild(cmt);
ele.replaceChild(txt2, txt);
txt.insertData(0, '789');
console.log(ele);
八,解决计算精度问题, 例如 0.1+0.2?
toFixed()问题:
- 返回的是字符串;
- 会强制保留限定小数位;
- 某些浏览器对于小数的进位有点不同;
console.log((0.1 + 0.2).toFixed(2));
网上流传的方法, 思路就是把数字转换整数然后再除回原位数:
/**
* floatTool 包含加减乘除四个方法,能确保浮点数运算不丢失精度
*
* 我们知道计算机编程语言里浮点数计算会存在精度丢失问题(或称舍入误差),其根本原因是二进制和实现位数限制有些数无法有限表示
* 以下是十进制小数对应的二进制表示
* 0.1 >> 0.0001 1001 1001 1001…(1001 无限循环)
* 0.2 >> 0.0011 0011 0011 0011…(0011 无限循环)
* 计算机里每种数据类型的存储是一个有限宽度,比如 JavaScript 使用 64 位存储数字类型,因此超出的会舍去。舍去的部分就是精度丢失的部分。*
* ** method **
* add / subtract / multiply /divide
*
* ** explame **
* 0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004)
* 0.2 + 0.4 == 0.6000000000000001 (多了 0.0000000000001)
* 19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002)
*
* floatObj.add(0.1, 0.2) >> 0.3
* floatObj.multiply(19.9, 100) >> 1990
*
*/
var floatTool = (function() {
/*
* 判断 obj 是否为一个整数
*/
function isInteger(obj) {return Math.floor(obj) === obj;
}
/*
* 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
* @param floatNum {number} 小数
* @return {object}
* {times:100, num: 314}
*/
function toInteger(floatNum) {
var ret = {
times: 1,
num: 0,
};
if (isInteger(floatNum)) {
ret.num = floatNum;
return ret;
}
var strfi = floatNum + '';
var dotPos = strfi.indexOf('.');
var len = strfi.substr(dotPos + 1).length;
var times = Math.pow(10, len);
var intNum = parseInt(floatNum * times + 0.5, 10);
ret.times = times;
ret.num = intNum;
return ret;
}
/*
* 核心方法,实现加减乘除运算,确保不丢失精度
* 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
*
* @param a {number} 运算数 1
* @param b {number} 运算数 2
* @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数
* @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
*
*/
function operation(a, b, op) {var o1 = toInteger(a);
var o2 = toInteger(b);
var n1 = o1.num;
var n2 = o2.num;
var t1 = o1.times;
var t2 = o2.times;
var max = t1 > t2 ? t1 : t2;
var result = null;
switch (op) {
case 'add':
if (t1 === t2) {
// 两个小数位数相同
result = n1 + n2;
} else if (t1 > t2) {
// o1 小数位 大于 o2
result = n1 + n2 * (t1 / t2);
} else {
// o1 小数位 小于 o2
result = n1 * (t2 / t1) + n2;
}
return result / max;
case 'subtract':
if (t1 === t2) {result = n1 - n2;} else if (t1 > t2) {result = n1 - n2 * (t1 / t2);
} else {result = n1 * (t2 / t1) - n2;
}
return result / max;
case 'multiply':
result = (n1 * n2) / (t1 * t2);
return result;
case 'divide':
return (result = (function() {
var r1 = n1 / n2;
var r2 = t2 / t1;
return operation(r1, r2, 'multiply');
})());
}
}
// 加减乘除的四个接口
function add(a, b) {return operation(a, b, 'add');
}
function subtract(a, b) {return operation(a, b, 'subtract');
}
function multiply(a, b) {return operation(a, b, 'multiply');
}
function divide(a, b) {return operation(a, b, 'divide');
}
// exports
return {
add: add,
subtract: subtract,
multiply: multiply,
divide: divide,
};
})();
九,递归闭包函数
function fun(n, o) {console.log(o);
return {fun: function(m) {return fun(m, n);
},
};
}
情况一
var a = fun(0) // undefined
a.fun(1) // 0
a.fun(2) // 0
a.fun(3) // 0
因为不断递归看起来会很迷糊, 我们试着把它拆分出现展示
n = 0;
o = undefined;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 1;
n = 0;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 2;
n = 0;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 3;
n = 0;
a = {fun: function(m) {return fun(m, n);
}
};
干扰代码很多, 但是实际上只有第一次执行的时候会赋值给 o, 后续调用都只是改变 n 值
情况二
var b = fun(0).fun(1).fun(2).fun(3);
// undefined
// 0
// 1
// 2
这个乍看之下和上面没什么区别, 但是结果挺诧异的, 我也想了好久, 还有一个纠结地方是我当时不记得执行方法和. 运算符谁的优先级比较高
n = 0;
o = undefined;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 1;
n = 0;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 2;
n = 1;
a = {fun: function(m) {return fun(m, n);
}
};
--------------------
m = 3;
n = 2;
a = {fun: function(m) {return fun(m, n);
}
};
最后想起情况一其实是属于闭包的用法, 还涉及到执行环境和作用域的知识, 因为它是属于执行完后只保存第一次变量 o, 所以除了第一次能够正常赋值之后后续都是只赋值 m, 所以输出结果都是 0
(详情可以参考我之前写的文章 Javascript 难点知识运用 — 递归, 闭包, 柯里化等 (不定时更新))
情况二就比较特殊, 因为他整个执行环境运行过程中都能够访问改变 o, 具体还是得靠自己理解一下, 我不知道怎么表达出来
情况三
var c = fun(0).fun(1)
c.fun(2)
c.fun(3)
// undefined
// 0
// 1
// 1
如果理解上面两种情况的话, 这题问题就简单了.