4

前言

2018/04/27 新增六,讲解浅拷贝和深拷贝的区别并简单实现, 七,原生JS操作DOM?
2018/04/30 新增八,解决计算精度问题,例如0.1+0.2?
2018/05/06 修改代码格式
2018/11/06 新增一个遇到的闭包执行环境的面试题
2019/12/15 新增

  • 十, 怎么实现一个简单的instanceof函数,
  • 十一, 怎么模拟Object.create的效果,
  • 十二, delete操作符的用法

一道常被人轻视的前端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新对象,优先级,原型链,继承,对象属性和原型属性等等.
(答案就不贴出来了,你们可以自己跑一下,怕你们忍不住先看正确答案.)

首先分析下上面都做了些什么。

  1. 定义一个Person函数,里面有一个getAge的匿名函数
  2. 为Person函数本身定义一个静态属性getAge函数
  3. 为Person函数原型上定义一个getAge函数
  4. 变量声明一个getAge函数表达式
  5. 直接声明一个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两种类型的区别:用函数声明创建的函数可以在函数解析后调用(解析时进行等逻辑处理);而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用。

变量对象的创建,依次经历了以下几个过程。

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为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();

  1. 前面直接执行Person(),返回一个对象指向全局window,所以不用考虑第二步函数本身定义静态属性,也就是window.getAge(),根据上一答知道函数声明会被覆盖,也不会进入到原型链搜索getAge函数,所以能排除第三步和第五步的干扰.
  2. 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();

理解了运算符优先级问题之后下面其实都好做
先来一步步剖析,

  1. 题目里成员访问(点符号)和new(带参数列表)最优先,同优先级情况下从左往右计算,所以先执行new Person();
  2. 因为Person函数内的getAge 只是一个赋值函数,所以所有实例都没有继承这个函数,只能从原型链上寻找到getAge函数,(输出30那个)
(new Person()).getAge();//30

new new Person().getAge();

承接上面步骤继续一步步剖析,

  1. 题目里成员访问(点符号)和new(带参数列表)最优先,同优先级情况下从左往右计算,所以先执行new Person();
  2. 因为Person函数内的getAge 只是一个赋值函数,所以所有实例都没有继承这个函数,只能从原型链上寻找到getAge函数,(输出30那个),所以先执行new Person().getAge;
  3. 将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. [1]先调用对象的valueOf()方法还是[1],再调用toString()方法输出"1",得出字符串再调用Number()返回1,
    true调用Number()直接返回1
  2. [[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.

然后可以做出大部分题型了

  1. 0==0
  2. null==0
  3. NaN==0
  4. 迷惑题,如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true;否则返回false(容易固定思维转化比较,尽管结果也对,但是不会转换类型比较的)
  5. 0==0
  6. 迷惑题,虽然都长得一样,但是引用类型指向地址不同就不会相等(详情可以参考我之前写的文章关于javascript基本类型和引用类型小知识)
  7. 迷惑题,加了取反符号之后就要考虑情况更多了,
    首先根据上一题答案里有讲解过符号优先级的问题,!优先级高于==,所以前面是一个判断,[] == true,取反就是false了,

    (![] ? true : false) == []  ->  0 == 0

    这题主要难点在于考虑符号优先级的问题,先判断再比较.

  8. 同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());

所以知道结果如下:

  1. new Boolean(true)先调用对象的valueOf()方法返回true,再调用toString()方法输出"true",得出字符串再调用Number()返回1
  2. 原理如上
  3. 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);

注意:

函数名调用好处:

  • 这个函数可以像其他任何代码一样在代码中调用。
  • 它不会污染名称空间。
  • 它的值不会改变。
  • 它性能更好(访问参数对象是昂贵的)。

深拷贝转格式写法:

序列化成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

如果理解上面两种情况的话,这题问题就简单了.

十, 怎么实现一个简单的instanceof函数

function instanceOf (instance, construct) {
  // 找出实例构造函数的原型
  var proto = instance.__proto__
  // 找出构造函数的原型
  var prototype = construct.prototype
  while (true) {
    // 最终点Object.prototype.__proto__的情况
    if (proto === null) {
      return false
    }
    // 匹配上原型的情况
    if (proto === prototype) {
      return true
    }
    // 都不满足的情况下往实例上一层原型链寻找
    proto = proto.__proto__
  }
}

function A () {
  return this
}
const a = new A()

console.log(instanceOf(a, A)) // true
console.log(instanceOf({}, A)) // false

十一, 怎么模拟Object.create的效果

function createOf (obj) {
  // 创建空构造函数
  function Fn () {}
  // 原型指向传入参数
  Fn.prototype = obj
  // 返回实例化的继承构造对象
  return new Fn()
}

var a = { a: 1 }
var b = createOf(a)

console.log(b.a) // 1

十二, delete操作符的用法

;(function (num) {
  delete num
  console.log(num)
})(1) // 1
  1. delete操作符只能用于对象身上的属性,对入参和函数无效
  2. delete只是切断引用,不会回收内存,想要达到效果需要明显赋值 null

Afterward
621 声望62 粉丝

努力去做,对的坚持,静待结果