前端开发

重学前端(二)- JavaScript

2019-03-19  本文已影响0人  番茄沙司a

JavaScript

理解面向对象

JavaScript 标准对基于对象的定义:“语言和宿主的基础设施由对象来提供,并且 JavaScript 程序即是一系列互相通讯的对象集合”。

  1. 什么是对象?

    Object(对象)在英文中,是一切事物的总称,这和面向对象编程的抽象思维有互通之处。对象是顺着人类的思维模式产生的一种抽象,于是,面向对象编程也被认为是更接近人类思维模式的一种编程范式。

  2. 人类的思维模式下,对象是什么?

    • 一个可以触摸或者可以看见的东西
    • 人的智力可以理解的东西
    • 可以指导思考或行动的东西
  3. 对象的本质特征?

    • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。

      体现:

      1. 对象具有唯一标识的内存地址
      2. 任何不同的JavaScript对象互不相等
    • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。

    • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。

  4. JavaScript对象?

    在 JavaScript 中将函数设计成一种特殊对象,将状态和行为统一抽象为“属性”。

    JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。

    为了提高抽象能力,JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性访问器属性(getter/setter)两类。

  5. JavaScript对象的两类属性?

    对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。

    • 数据属性:比较接近于其他语言的属性概念。

      四个特征:

      • value:属性的值
      • writable:决定属性能否被赋值。
      • enumerable:决定 for in 能否枚举该属性。
      • configurable:决定该属性能否被删除或者改变特征值。
    • 访问器属性:getter/setter

      四个特征:

      • getter:函数或 undefined,在取属性值时被调用。
      • setter:函数或 undefined,在设置属性值时被调用。
      • enumerable:决定 for in 能否枚举该属性。
      • configurable:决定该属性能否被删除或者改变特征值。

      访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖

    查看属性

    通常用于定义属性的代码会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true。可以使用内置函数 **Object.getOwnPropertyDescripter **来查看,如以下代码所示:

    var o = { a: 1 };
    o.b = 2;
    //a 和 b 皆为数据属性
    Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
    

    改变属性

    改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty,示例如下:

    var o = { a: 1 };
    Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
    //a 和 b 都是数据属性,但特征值变化了
    Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
    o.b = 3;
    console.log(o.b); // 2
    

    创建访问器属性

    在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性,代码如下所示:

    var o = { get a() { return 1 } };
    console.log(o.a); // 1
    

这样,我们就理解了,实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。

对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用 key 来查找 value 的字典)。以上面的对象 o 为例,可以想象一下“a”是 key。{writable:true,value:1,configurable:true,enumerable:true} 是 value。

JavaScript对象的特色

  1. 能够以 Symbol 为属性名
  2. 提供了完全运行时的对象系统,可以模仿面向对象编程范式
  3. 其对象系统具有高度动态性

理解模拟类

模拟基于类的面向对象

class关键字

原型系统也是优秀的抽象对象的形式

理解堆栈

stack自动分配的内存空间,它由系统自动释放;而heap则是动态分配的内存,大小不定也不会自动释放。

事件流

事件流描述的是从页面中接受时间的顺序。

dom事件流

事件捕获阶段,处于目标阶段,事件冒泡阶段

事件捕获的作用:focus,blur

Array的操作方法

归并方法:reduce、reduceRight

迭代方法:every,some,forEach,map,filter

位置方法:indexOf

其他操作:push,pop,shift,unshift,reverse,concat,slice,splice,

函数防抖和函数节流

总之都是为了节省计算资源。

函数防抖(debounce)

场景:

是指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。

基本思想:通过闭包保存一个标记(timeout)来保存 setTimeout 返回的值,每当用户输入的时候把前一个 setTimeout clear 掉,然后又创建一个新的 setTimeout,这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数了。

函数防抖的要点:也是需要一个setTimeout来辅助实现。延迟执行需要跑的代码。
如果方法多次触发,则把上次记录的延迟执行代码用clearTimeout清掉,重新开始。
如果计时完毕,没有方法进来访问触发,则执行代码。

// 函数防抖
function debounce(handlerFunc, interval = 300) {
    let timeout = null;
    return function () {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            handlerFunc.apply(this, arguments);
        }, interval);
    };
}

函数节流(throttle)

场景:过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。比如:onresize、onscroll、mousemove等。

为了避免类似问题,就可以使用定时器对该函数进行节流。

基本思想:某些代码不可以在没有间断的情况下连续重复执行,就是一定时间内函数只执行一次。

第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器尚未执行,就是将其替换为一个新的定时器,目的是只有在执行函数的请求停止了一段时间之后才执行。

函数节流的要点:声明一个变量(resizeTimeout)当标志位,记录当前代码是否在执行。

注意:只要是代码周期性执行的,都应该使用节流,但是并不能控制请求执行的速率。

// 函数节流
function throttle(handlerFunc, timeout = 66) {
  let resizeTimeout;
  if (!resizeTimeout) {
    resizeTimeout = setTimeout(() => {
      resizeTimeout = null;
      handlerFunc();
      // The actualResizeHandler will execute at a rate of 15fps
    }, timeout);
  }
}
window.addEventListener('resize', () => {
    throttle(this.onResize, 40)
}, false);

方法重载

function overLoading() {
    if (arguments.length == 1) {
        //操作1
    } else if (arguments.length == 2) {
        //实现重载
    }
}

overLoading(10);
overLoading(10, 20);
//addMethod
function addMethod(object, name, fn) {
    //把前一次添加的方法存在一个临时变量old里面
  var old = object[name];
    // 重写了object[name]的方法
  object[name] = function() {
      // 如果调用object[name]方法时,传入的参数个数跟预期的一致,直接调用
    if(fn.length === arguments.length) {
      return fn.apply(this, arguments);
        // 否则,判断old是否是函数,如果是,就调用old
    } else if(typeof old === "function") {
      return old.apply(this, arguments);
    }
  }
}
 
var people = {
  values: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};
 
/* 下面开始通过addMethod来实现对people.find方法的重载 */
 
// 不传参数时,返回peopld.values里面的所有元素
addMethod(people, "find", function() {
  return this.values;
});
 
// 传一个参数时,按first-name的匹配进行返回
addMethod(people, "find", function(firstName) {
  var ret = [];
  for(var i = 0; i < this.values.length; i++) {
    if(this.values[i].indexOf(firstName) === 0) {
      ret.push(this.values[i]);
    }
  }
  return ret;
});
 
// 传两个参数时,返回first-name和last-name都匹配的元素
addMethod(people, "find", function(firstName, lastName) {
  var ret = [];
  for(var i = 0; i < this.values.length; i++) {
    if(this.values[i] === (firstName + " " + lastName)) {
      ret.push(this.values[i]);
    }
  }
  return ret;
});
 
// 测试:
console.log(people.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(people.find("Dean")); //["Dean Edwards", "Dean Tom"]
console.log(people.find("Dean Edwards")); //["Dean Edwards"]

继承

  1. 原型链

    问题:1. 引用类型的属性被所有实例共享

    ​ 2. 在创建 Child 的实例时,不能向Parent传参

  2. 借用构造函数

    优点:

    1.避免了引用类型的属性被所有实例共享

    2.可以在 Child 中向 Parent 传参

    缺点:

    方法都在构造函数中定义,每次创建实例都会创建一遍方法。

  3. 组合继承

    优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

  4. 原型式继承

    就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

    缺点:

    包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样

  5. 寄生式继承

    创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

    缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

  6. 寄生组合式继承

    这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

Promise

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Promise 构造函数包含一个参数和一个带有 resolve(解析)和 reject(拒绝)两个参数的回调。在回调中执行一些操作(例如异步),如果一切都正常,则调用 resolve,否则调用 reject。

与普通旧版 JavaScript 中的 throw 一样,通常拒绝时会给出 Error 对象,但这不是必须的。Error 对象的优点在于它们能够捕捉堆叠追踪,因而使得调试工具非常有用。

深拷贝和浅拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

浅拷贝的实现方式

1、可以通过简单的赋值实现

类似上面的例子,当然,我们也可以封装一个简单的函数,如下:

function simpleClone(initalObj) {
    var obj = {};
    for (var i in initalObj) {
        obj[i] = initalObj[i];
    }
    return obj;
}

var obj = {
    a: "hello",
    b: {
        a: "world",
        b: 21
    },
    c: ["Bob", "Tom", "Jenny"],
    d: function () {
        alert("hello world");
    }
}
var cloneObj = simpleClone(obj);
console.log(cloneObj.b);
console.log(cloneObj.c);
console.log(cloneObj.d);

cloneObj.b.a = "changed";
cloneObj.c = [1, 2, 3];
cloneObj.d = function () {
    alert("changed");
};
console.log(obj.b);
console.log(obj.c);
console.log(obj.d);

2、ES6的Object.assign()实现

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

var obj = { a: {a: "hello", b: 21} };

var initalObj = Object.assign({}, obj);

initalObj.a.a = "changed";

console.log(obj.a.a); //  "changed"

注意:当object只有一层的时候,是深拷贝,例如如下:

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

深拷贝的实现方式

1、方法一还是手动复制

和上面的举例一样,手动复制可以实现深拷贝。

2、对象只有一层的话可以使用:

   var obj1 = { body: { a: 10 } };
   var obj2 = JSON.parse(JSON.stringify(obj1));
   obj2.body.a = 20;
   console.log(obj1);
   // { body: { a: 10 } } <-- 沒被改到
   console.log(obj2);
   // { body: { a: 20 } }
   console.log(obj1 === obj2);
   // false
   console.log(obj1.body === obj2.body);
   // false

用JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。

可以封装如下函数:

var deepClone = function (obj) {
    var str, newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== 'object') {
        return;
    } else if (window.JSON) {
        str = JSON.stringify(obj), newobj = JSON.parse(str);
    } else {
        for (var i in obj) {
            newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i];
        }
    }
    return newobj;
};

4、递归拷贝

function deepClone(obj) {
    var newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== 'object') {
        return;
    }
    for (var i in obj) {
        newobj[i] = typeof obj[i] === 'object' ?
        deepClone(obj[i]) : obj[i];
    }
    return newobj;
}
var copyArray = deepClone(array);

5、使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。

   function deepClone(initalObj, finalObj) {    
     var obj = finalObj || {};    
     for (var i in initalObj) {        
       var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
       if(prop === obj) {            
         continue;
       }        
       if (typeof prop === 'object') {
         obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
       } else {
         obj[i] = prop;
       }
     }    
     return obj;
   }

6、jquery

jquery 有提供一个$.extend可以用来做 Deep Copy。

   var $ = require('jquery');
   var obj1 = {
       a: 1,
       b: { f: { g: 1 } },
       c: [1, 2, 3]
   };
   var obj2 = $.extend(true, {}, obj1);
   console.log(obj1.b.f === obj2.b.f);
   // false

7、lodash

另外一个很热门的函数库lodash,也有提供_.cloneDeep用来做 Deep Copy。

   var _ = require('lodash');
   var obj1 = {
       a: 1,
       b: { f: { g: 1 } },
       c: [1, 2, 3]
   };
   var obj2 = _.cloneDeep(obj1);
   console.log(obj1.b.f === obj2.b.f);
   // false

这个性能还不错,使用起来也很简单。

JavaScript 深拷贝性能分析:(来源:迷渡)

https://zhuanlan.zhihu.com/p/33489557

结论:

介绍一下作用域链,作用域链的尽头是什么

作用域:是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。每一个函数都有一个作用域,比如我们创建了一个函数,函数里面又包含了一个函数,那么就有三个作用域,这样就形成了一个作用域链。

作用域的特点:变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。内层的变量可以shadow外层的同名变量。

作用域链的尽头:全局作用域

作用域种类:函数作用域、全局作用域。

koa和express的区别

Koa

koa是一个相对于express来说更小、更健壮、更富表现力的Web框架,不用写回调。它的所有东西都通过插件实现,符合Unix哲学,即简单原则。最大的特点是独特的中间件流程控制,是一个典型的洋葱模型。koa和koa2中间件的思路是一样的,但是实现方式有所区别,koa2在node7.6之后更是可以直接用async/await来替代generator使用中间件。

[图片上传失败...(image-c1a6b8-1550406189822)]

app.listen使用了this.callback()来生成node的httpServer的回调函数。

this.middleware是中间件的集合

koa的实现有几个最重要的点:

  1. context的保存和传递
  2. 中间件的管理和next的实现

洋葱模型的实现核心:

  1. context一路传下去给中间件
  2. middleware中的下一个中间件fn作为未来next的返回值

详解中间件:https://github.com/qianlongo/resume-native/issues/1

执行顺序:koa是从第一个中间件开始执行,遇到next进入下一个中间件,一直执行到最后一个中间件,在逆序。

koa.js 是下一代的node.js框架,由Express团队开发,通过生成器(generators JavaScript 1.7新引入的,用于解决回调嵌套的方案),减少异步回调,提高代码的可读性和可维护性,同时改进了错误处理。

判断数组

讲一下闭包和闭包常用场景

应用场景:设置私有变量和方法让这些变量的值始终保持在内存中还有读取函数内部变量。通过创建另一个匿名函数强制让闭包的行为符合预期。

不适合场景:返回闭包的函数是个非常大的函数。

闭包的缺点

缺点:一旦有变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,常驻内存,会增大内存使用量,很容易造成内存泄露。所以有必要将引用变量设置为null,解除引用,确保正常回收其占用的内存。

为什么会出现闭包这种东西,解决了什么问题

受JavaScript链式作用域结构的影响,父级变量中无法访问到子级的变量值,为了解决这个问题,才使用闭包这个概念。

javaScript中的this是什么,有什么用,它的指向是什么

null==undefined 为什么为true

undefined派生自null值

判断回文数

var isPalindrome = function (x) {
    // negative number can't be palidrome
    if (x < 0) return false;

    return x.toString() === x.toString().split('').reverse().join('');
};

快排

function quickSort(arr) {
  const pivot = arr.shift();
  const left = [];
  const right = [];

  if (arr.length < 2) { return arr; }

  arr.forEach((element) => {
    element < pivot ? left.push(element) : right.push(element);
  });

  return quickSort(left).concat([pivot], quickSort(right));
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort2(arr));

注意事项:基准的选择,如何更接近中间值。

JS类型(运行时)

[图片上传失败...(image-288a3f-1550406189822)]

运行时类型是代码实际执行过程中用到的类型。所有的类型数据都会属于以下7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程产生的数据,都有运行时类型。

JavaScript 语言的每一个值都属于某一种数据类型。JavaScript语言定义了7种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Symbol
  7. Object

Object为引用数据类型,其他均为原始数据类型。

JS的类型转换

  1. 显式类型转换
  2. 隐式类型转换

因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。

JS 中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住,属于设计失误,最好先进行显式类型转换后,用===比较。

其它运算,如加减乘除大于小于,也都会涉及类型转换。幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:

[图片上传失败...(image-57e9a5-1550406189822)]

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

此外,JavaScript 支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的 e 来表示:

注意:parseInt 和 parseFloat 并不使用这个转换。在不传入第二个参数的情况下,parseInt 只支持 16 进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。

在一些古老的浏览器环境中,parseInt 还支持 0 开头的数字作为 8 进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入 parseInt 的第二个参数,而 parseFloat 则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

在较小的范围内,数字到字符串的转换是完全符合直觉的十进制表示。当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这个算法细节繁多,从感性的角度认识,它其实就是保证了产生的字符串不会过长。

装箱转换

每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱

我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。我们可以用 console.log 看一下这个东西的 type of,它的值是 object,我们使用 symbolObject instanceof 可以看到,它是 Symbol 这个类的实例,我们找它的 constructor 也是等于 Symbol 的,所以我们无论从哪个角度看,它都是 Symbol 装箱过的对象:

var symbolObject = (function(){ return this; }).call(Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

注意:装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在 JavaScript 代码中显式调用装箱能力。

var symbolObject = Object((Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

var symbolObject = Object((Symbol("a"));

console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call 本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o * 2
// valueOf
// toString
// TypeError

定义一个对象 o,o 有 valueOf 和 toString 两个方法,这两个方法都返回一个对象,然后我们进行 o*2 这个运算的时候,你会看见先执行了 valueOf,接下来是 toString,最后抛出了一个 TypeError,这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从 o*2 换成 o + “”,那么你会看到调用顺序就变了。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o + ""
// toString
// valueOf
// TypeError

到 String 的拆箱转换会优先调用 toString。

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}

o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


console.log(o + "")
// toPrimitive
// hello

Undefined、Null

  1. 为什么有的编程规范要求用 void 0 代替 undefined?

    Undefined表示未定义,它的类型只有一个值,就是undefined。任何变量在赋值前都是Undefined类型、值为undefined,一般我们可以用全局变量undefined来表达这个值,或者用void运算把任一个表达式变成undefined值。

    JavaScript公认的设计失误之一是undefined是一个变量,而非一个关键字。所以,为了防止无意中篡改,建议使用void 0 来获取 undefined 的值

  2. Undefined和null差别?

    null:定义了但是为空。在实际编程中,一般不会把变量赋值为undefined,这样可以保证所有值为undefinded的变量,都是从未赋值的自然状态。

    Null类型也只有一个值 null,它的语义表示空值,与undefined不同,null是JavaScript关键字,所以在任何代码中,都可以放心用null关键字来获取null值。

Boolean

有两个值true、false,用于表示逻辑意义上的真和假,同样有关键字true和false来表示两个值。

String

  1. 字符串有最大长度吗?

    String 用于表示文本数据。String 有最大长度是 2^53 - 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是理解中的字符数。

    因为 String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

    Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。

    JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。

    JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。

Number

下面,我们来说说 Number 类型。Number 类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数,当然,在计算机中,我们有一定的精度限制。

JavaScript 中的 Number 类型有 18437736874454810627(即 264-253+3) 个值。

JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是 JavaScript 为了表达几个额外的语言场景(比如不让除以 0 出错,而引入了无穷大的概念),规定了几个例外情况:

另外,值得注意的是,JavaScript 中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以 -0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义,Number 类型中有效的整数范围是 -0x1fffffffffffff 至 0x1fffffffffffff,所以 Number 无法精确表示此范围外的整数。

同样根据浮点数的定义,非整数的 Number 类型无法用 ==(=== 也不行) 来比较,一段著名的代码,这也正是为什么在 JavaScript 中,0.1+0.2 不能 =0.3:这是浮点运算的特点,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。

正确比较浮点型的方法

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

Symbol

Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象 key 的集合,在 ES6 规范中,整个对象系统被用 Symbol 重塑。

Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol 也不相等。

创建 Symbol 的方式是使用全局的 Symbol 函数。例如:

var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为

var o = new Object

o[Symbol.iterator] = function () {
    var v = 0
    return {
        next: function () {
            return {
                value: v++,
                done: v > 10
            }
        }
    }
};

for (var v of o)
    console.log(v); // 0 1 2 3 ... 9

这里给对象 o 添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个 0 到 10 的迭代器,之后可以在 for of 中使用这个 o 对象。

这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。

Object

Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object 表示对象的意思,它是一切有形和无形物体的总称。

为什么给对象添加的方法能用在基本类型上?

答案

运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

解释

在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性访问器属性,二者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。

提到对象,我们必须要提到一个概念:类。

C++和Java中,每个类都是一个类型,而JavaScript中的“类”仅仅是运行时对象的一个私有属性,JavaScript中是无法自定义类型的。

JavaScript 中的几个基本类型,都在对象类型中有一个映射。它们是:

所以,3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换

�Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。

JavaScript 语言设计上试图模糊对象和基本类型之间的关系,日常代码可以把对象的方法在基本类型上使用,比如:

console.log("abc".charAt(0)); //a

甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了 hello 方法,在任何 Symbol 类型变量都可以调用。

Symbol.prototype.hello = () => console.log("hello");

var a = Symbol("a");
console.log(typeof a); //symbol,a 并非对象
a.hello(); //hello,有效

规范类型

如果不用原生的Number和parseInt,用JS代码实现String到Number的转换,该怎么做?

const stringToNumber = str => (typeof str === 'string') ? +str : new TypeError(`${str} is not a string`)
const source = "123456789";
let result = 0

for (let idx = source.length - 1; idx >= 0; idx--) {
    result += source[idx] * Math.pow(10, (source.length - idx - 1));
}

typeof运算结果与运行时类型的规定对比

[图片上传失败...(image-8bd01b-1550406189822)]

注意 object——Null 和 function——Object 是特例。

如何取出一个数组里的图片并按顺序显示出来

function loadImage(imgList, callback) {
    if (!Array.isArray(imgList) || callback.constructor !== Function) {
        console.log('lost');
        return;
    }
    var imageData = [];
    imgList.forEach(function (src, i) {
        var img = new Image();
        img.onload = function () {
            body.appendChild(imageData.shift());
            if (!imageData.length) {
                callback();
                return;
            }
            this.onload = null;
        };
        img.src = src;
        imageData.push(img);
    });
};

模拟队列

说说你知道JavaScript的内存回收机制

垃圾回收器会每隔一段时间找出那些不再使用的内存,然后为其释放内存。

一般使用标记清除方法 当变量进入环境标记为进入环境,离开环境标记为离开环境

还有引用计数方法

手动封装jsonp

const jsonp = function (url, data) {
    return new Promise((resolve, reject) => {
        // 初始化url
        let dataString = url.indexOf('?') === -1 ? '?' : '&'
        let callbackName = `jsonpCB_${Date.now()}`
        url += `${dataString}callback=${callbackName}`
        if (data) {
            // 有请求参数,依次添加到url
            for (let k in data) {
                url += `&${k}=${data[k]}`
            }
        }
        let jsNode = document.createElement('script')
        jsNode.src = url
        // 触发callback,触发后删除js标签和绑定在window上的callback
        window[callbackName] = result => {
            delete window[callbackName]
            document.body.removeChild(jsNode)
            if (result) {
                resolve(result)
            } else {
                reject('没有返回数据')
            }
        }
        // js加载异常的情况
        jsNode.addEventListener('error', () => {
            delete window[callbackName]
            document.body.removeChild(jsNode)
            reject('JavaScript资源加载失败')
        }, false)
        // 添加js节点到document上时,开始请求
        document.body.appendChild(jsNode)
    })
}
jsonp('http://192.168.0.103:8081/jsonp', {
        a: 1,
        b: 'heiheihei'
    })
    .then(result => {
        console.log(result)
    })
    .catch(err => {
        console.error(err)
    })

注意:用完要删除script和引入的全局变量

如果函数没有指定形参或者实际参数大于形参数,该如何处理。分别用ES5和ES6处理

ES5:

ES6:

const numbers = (...nums) => nums;

JavaScript常见API

https://juejin.im/entry/58bccbef44d904006ae8eb09

求数组最大值和最小值之差

  1. 使用归并方法reduce (ES5)

    API:array.reduce(function(prev, cur,index, array), initialValue);

    有两个参数:

    • 在每一个项上调用的函数

      函数有四个参数:

      • prev:初始值
      • cur:当前元素
      • index:当前元素的索引
      • array:当前元素所属的数组对象
    • 作为归并基础的初始值(可选)

    var arr = [567, 45, 3, 3, 6, 2, 7, 234, 56];
    
    function getMaxSubMin(arr) {
        Array.prototype.max = function () {
            return this.reduce(function (preValue, curValue, index, array) {
                return preValue > curValue ? preValue : curValue;
            })
        }
        Array.prototype.min = function () {
            return this.reduce(function (preValue, curValue, index, array) {
                return preValue > curValue ? curValue : preValue;
            })
        }
        return arr.max() - arr.min();
    };
    console.log(getMaxSubMin(arr));//565
    
  2. 使用内置函数的数学方法Math.max()Math.min()

    var arr = [567, 45, 3, 3, 6, 2, 7, 234, 56];
    
    function getMaxSubMin(arr) {
        Array.prototype.max = function () {
            return Math.max.apply({}, this);
        };
        Array.prototype.min = function () {
            return Math.min.apply({}, this);
        };
        return arr.max() - arr.min();
    };
    console.log(getMaxSubMin(arr)); //565
    

日期格式的数组如何排序

var dateArr = ['2018-01-20', '2017-02-19', '2016-08-08', '2018-12-09'];

function sortByDate(dateArr) {
    dateArr.sort(function (a, b) {
        return Date.parse(b.replace(/-/g, "/")) - Date.parse(a.replace(/-/g, "/"));
    });
    return dateArr;
}
console.log(sortByDate(dateArr));
/* [ 
'2018-12-09',
'2018-01-20',
'2017-02-19',
'2016-08-08' ] */

点击ul底下任意li,输出对应的li里面的内容

事件委托:利用冒泡的原理,把事件加到父级上,触发执行效果。

window.onload = function () {
    var myul = document.querySelector('ul');
    var list = document.querySelectorAll('ul li');

    myul.addEventListener('click', function (ev) {
        var ev = ev || window.event;
        var target = ev.target || ev.srcElemnt;

        for (let i = 0, len = list.length; i < len; i++) {
            if (list[i] == target) {
                alert(target.innerHTML);
            }
        }
    });
}

请为所有数组对象添加一个findDuplicate(n)方法,用于返回该数组中出现频率>=n的元素列表。

Array.prototype.findDuplicate = function (count) {
    return this.reduce((re, val) => {
        let index = re.findIndex(o => o.val === val)
        if (index >= 0) {
            re[index].count++
        } else {
            re.push({
                count: 1,
                val
            })
        }
        return re
    }, []).filter(o => o.count >= count).map(o => o.val)
}

封装一个repeat函数,调用console.log4次hello world,每次间隔3秒

function repeat (func, times, wait) {
    return function() {
        var i = 0, arg = arguments;
        var handle = function (i) {
            setTimeout(function () {
                func.apply(null, arg);
            }, wait*i)
        }
        for (let i = 0; i < times; i++) {
            handle(i);
        }
    }
}
var repeatFunc = repeat(console.log, 4, 3000);
repeatFunc("hellworld");

尾递归

尾部调用自身。

优点:由于只存在一个调用帧,所以永远不会出现“栈溢出”错误。节省内存。

尾递归优化计算Fibonacci数列:

function Fib(n, ac1 = 1, ac2 = 1) {
    if (n <= 1) {
        return ac2;
    };
    return Fib(n - 1, ac2, ac1 + ac2);
}

异步编程

实现输出一个数组中第N大的数据(堆排序)

该实现只需循环k次。非常适合内存有限、数据海量的情况。时间复杂度:O(N * log2K)

function topKMaxOfArr(k, arr) {
    function swap(a, b) {
        var t = arr[a];
        arr[a] = arr[b];
        arr[b] = t;
    }
    var i, j;
    for (i = arr.length; i > arr.length - k; i--) {
        for (j = Math.floor(i / 2) - 1; j >= 0; j--) {
            if (arr[j] < arr[2 * j + 1]) {
                swap(j, 2 * j + 1);
            }
            if (2 * j + 2 < i && arr[j] < arr[2 * j + 2]) {
                swap(j, 2 * j + 2);
            }
        }
        swap(i - 1, 0);
    }
    return arr.slice(arr.length - k);
}

var arr = [1, 4, 6, 8, 99, 77, 56, 10, 25, 20],
    k = 5;
console.log(topKMaxOfArr(k, arr)); //[ 20, 25, 56, 77, 99 ]

写一个快速排序

function quickSort(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 quickSort(left).concat([pivot], quickSort(right));
}
var arr = [12, 34, 45, 65, 1, 2, 3];
console.log(quickSort(arr));

图片预加载和懒加载

预加载:

function getPreloadImgAttr(url,callback){
    var oImg = new Image(); //创建一个Image对象,实现图片的预加载
    oImg.src = url;      // 看下一节,其实应当先进行onload的绑定,再赋值给src
    if(oImg.complete){
        //如果图片已经存在于浏览器缓存,直接调用回调函数
        callback.call(oImg);
        return; //直接返回,不再处理onload事件
    }
    oImg.onload = function(){
        //图片下载完毕时异步调用callback函数
        callback.call(oImg);   
    };
}
getPreloadImgAttr('image/example.jpg',function(){
    console.log(this.width, this.height);
});

懒加载:

当网页滚动的事件被触发 -> 执行加载图片操作 -> 判断图片是否在可视区域内 -> 在,则动态将data-src的值赋予该图片。(将图片src赋值为一张默认图片,当用户滚动滚动条到可视区域图片时候,再去加载真正的图片)

有限状态机 ( FSM )

表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。简单的来说,它有三个主要特征:

  1. 状态总数 ( state ) 是有限的
  2. 任一时刻,只处在一种状态之中
  3. 某种条件下,会从一种状态转变 ( transition ) 到另一种状态

其实生成器就是通过暂停自己的作用域 / 状态来实现它的魔法的。

https://juejin.im/post/5bcad206f265da0ac3734014

上一篇 下一篇

猜你喜欢

热点阅读