JS模块-速记版本总结

339 阅读26分钟

JS数据类型

简单数据类型:null,undefined,number, boolean,string,symbol,BigInt
引用数据类型:Object、array、function、Date、regexp

类型转换机制

何时触发

运算类型与预期不符

类型 -> 强制转换(显式转换)

Number(),parseInt(),String(),Boolean()

1.Number()

严,若一个字符转不了,字符串为NaN
Symbol-> Throw a TypeError exception
Object -> 先调用toPrimitive,再调用toNumber\

'' -> 0
null -> 0
undefined -> NaN
'324',324 -> 324
true,false -> 1,0
'324abc' -> NaN
{a,1} -> NaN
[1,2,3] -> NaN
[5] -> 5
[] -> 0

2.parseInt()

逐解析字符,遇不能转停 parseInt('234ab3') -> 234

3.String()

将任字符转字符串
Symbol-> Throw a TypeError exception
Object -> 先调用toPrimitive,再调用toNumber

null -> 'null'
undefined -> 'undefined'
true,false -> 'true','false'
1 -> '1'
'a' -> 'a'
{a:1} -> '[object Object]'
[1,2,3] -> '1,2,3'

4.Boolean()

null -> false
undefined -> false
0 -> false
NaN -> false
'' -> false
{},[] -> true
new Boolean(false) -> true

类型 -> 自动转换(隐式转换)

比较运算:== != > < if while
算数运算:+-*/%

1.自动转换为布尔值:

null undefined false +0 -0 '' NaN都会转化为false,其他为true\

2.自动转化为字符串:

遇到预期为字符串的地 -> 非字符转化为字符
复合类型->原始类型->字符串

  • 遇到字符会进行字符拼接
    '5' + 1 = '51'
    '5' + true = '5true'
    '5' + false = '5false'
    '5' + {} = '5[object Object]'
    '5' + [] = '5'
    '5' + function(){} = '5function(){}'
    '5' + undefined = '5undefined'
    '5' + null = '5null'

三 自动转换为数值

'+'可能把运算子转为字符串,其他运算符可能会把运算子转为数值。
'5'-'2' = 3
'5' * '2' = 10
true - 1 = 0
false - 1 = -1
'1' - 1 = 0
'5' * [] = 0
false / '5' = 0
'abc' - 1 = NaN
null + 1 = 1
undefined + 1 = NaN

[] == ![]结果是什么?

[]->0 ![] -> []引用数据类型转化为boolean为true ->![]->false->0

最终结果为true

Js数组

标题
会对原数组影响push
unshift(添多个开头)
splice(开位置,删数量,插元)
pop
shift(删最前一项)
splice(start,删除数量)
splice(start, 删除数量,插元)indexOf() 返位置,未找到返-1
includes()
reverse 反向排列
sort 接收一个比较函,判哪个排前image.png
不会影响concat(创副本,添到副末)slice(start,end)

ES6数组新增的拓展方法

构造函数方法查找遍历展平迭代循环填充
Array.from()
Array.of()
find()
findIndex()
keys()
values()
entries
flat()
flatMap()
some
every
forEach
filter
map
foroffill()

for(let i of arr.keys()){}
for(let i of arr.values()){}
for(let i of arr.entries()){}

类数组转换为数组的方法

call调Array的slice, splice Array.prototype.slice.call(arrayLike) Array.prototype.splice.call(arrayLike, 0)

apply调Array的concat Array.prototype.concat.apply([],arrayLike) Array.prototype.slice.apply(arguments) Array.from(arrayLike)

arguments不是真正数组,无slice方法,通过apply可以调用数组slice方法,将arguments 转类数组为数组

document.querySelectorAll(' * ')得到一个NodeList,需转化为数组 [...document.querySelectorAll('*')]

字符串的方法

遍历模版匹配转换
+,${}拼接,concatslice(start, end)
substr(start, length)
substring(start, end)
indexOf
includes
charAt
startWith
forofmatch
search
replace
split

ES5对象的方法

Object.keys()
Object.getOwnPropertyNames
Object.getPrototypeOf() 获取一个对象的原型对象

ES6对象新增的拓展方法

Object.is(a,b) 严格判断两个值是否相等

image.png

Object.assign()浅拷贝,合并对象,遇到同名属性会进行替换
Object.setPrototypeOf()设置一个对象的原型对象
Object.values()
Object.entries()

image.png
Object.fromEntries()

image.png

原型链,面向对象和继承

原型:

js基于原型语言,每对象拥原型对象。
访某对象原型时,从对象搜,还从原型搜,以及对象原的原,层层向上,直到找到匹配属性或到达原型链末尾。
这些属性和方法定义在对象的构造器函数的prototype上。
函数可以有属性,叫原型prototype

原型链:

原型对象也拥有原型,从中继属和方,一层一层往上找以此类推的关系称原型链。解释了为何一个对象会有其他对象的属和方

总结:

image.png

image.png

每个对象_proto_指向它构造函数的原型对象prototypeperson._proto__ = Person.prototype
构造函数是一个函数对象,是通过Function构造器产生Person._proto_ = Function.prototype
原型对象是普通对象,普对构造函数都是ObjectPerson.prototype._proto_ = Object.prototype
构造器是函对象,函数对象由Function构造产生Object._proto_ === Function.prototype
Object原型对象也有_proto_属性,指向null,null是原型链的顶端Object.prototype._proto_ = null

对象都继自Object,Object对象继自根对象null
函数对象都继承自Function对象

Function对象的_proto_会指向自己的原型对象,最终指向Object的原型对象
Function._proto_ === Function.prototype
Function.prototype._proto_ === Object.prototype
Object.prototype._proto_ === null
__proto__可以理解为继承自,或者指向的意思

对象?为什么要面向对象?

特点:面向对象: 逻辑迁移灵活、高复用性、高模块化
理解:对象为对单物体简抽象
一个容器,封了属&方
** 属性: 对象状态

//简单对象
const Course = {
    teacher: 'AA',
    leader: 'BB',
    startCourse: function (name) {
        return `开始${name}课程`;
    }
}
//函数对象
function Course () {
    this.teacher = 'AA';
    this.leader = 'BB';
    this.startCourse = function (name) {
        return `开始${name}课程`;
    }
}

构造函数 - 生成对象

** 一个模板 - 表一类物体的共同特征,生成对象
** 类即对象模板
** js其实本质不基于类,而基于构造函数 + 原型链
** constructor + prototype

Course本质是构造函数

** 函数里用this来指向实例对象
** new实例化生对象
** 可做初始化传参

    function Course () {
        this.teacher = 'AA';
        this.leader = 'BB';
    }
    const course = new Course();

追问:

** 构造函数,不初始化,可以使用么 - 无法使用
** 如果需要使用,如何做兼容

    function Course () {
        const isClass = this instanceof Course; //刚开始的this指向window
        if(!isClass) {
            return new Course(); // 这里再次调用Course方法,把this指向Course
        }
        this.teacher = 'AA';
        this.leader = 'BB';
    }
    const course = Course();

追问:new 是什么 / new的原理 / new做了什么

** 创了一个空对象,作为返的对象实例
** 将空对象__proto__指向构造函数prototype(将对象与构函通过原型链连接)
** 将构造函数中this指向当前实对
** 执构函初化

image.png

根据构函数返类型做判断,若原始值忽略,若返对象则正常处理。

构造函数返回值

function Test(name) {
this.name = name
return 1  ---------> 构造函数返回的‘原始值’没用
}
const test = new Test('xxx')
console.log(test.name) // xxx

function Test2(name) {
this.name = name
console.log(this) // Test2 {name: 'xxx'}
return {age: 18}  ----------> 构造函数返回的‘对象’有用
}
const test2 = new Test('xxx')
console.log(test2) // {age: 18}
console.log(test2.name) // undefined

箭头函数

new一个箭头函数

箭函ES6中提

  1. 没prototype
  2. 没this指向
  3. 不可使用arguments参数
  4. 不能New

箭头函数的this指向

没⾃this,它的this是其上下⽂this作为⾃this值。所以不会被new调⽤,this也不会改变。

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); // true
    }; 
  } 
}

追问: 实例属性影响

    function Course (teacher, leader) {
        this.teacher = teacher;
        this.leader = leader;
    }
    const course1 = new Course('AA', 'BB');
    const course2 = new Course('AA', 'CC');
    course2.leader = '可可'; // course1.leader不会受到影响

constructor

function Course(teacher, leader) {
  this.teacher = teacher;
  this.leader = leader;
}
const course = new Course('AA', 'BB');

** 对象创时会自动拥一个构函属性constructor
** constructor继承自原型对象,指向构造函数的引用
** course.constructor === course._proto_.constructor === Course.prototype.constructor (constructor继承自原型对象)
** course.constructor === Course (指向构造函数的引用)

使用构造函数没有问题么?会有什么性能问题?

  function Course(name) {
    this.teacher = 'AA',
    this.leader = 'BB',
    this.startCourse = function(name) {
      return `开始${name}课`;
    }
  }
  const course1 = new Course('es6');
  const course2 = new Course('OOP');
  // 构造函数中的方法,会存在于每个生成的实例中,重复挂载会导致资源浪费
  //优化

  function Course(name) {
    this.teacher = 'AA';
    this.leader = 'BB';
  }
  //避免了构造函数中的方法被重复挂载,并且在原型对象的所有属性和方法,都能被实例所共享
  Course.prototype.startCourse = function(name) { 
    return `开始${name}课`;
  }

  const course1 = new Course('es6');
  const course2 = new Course('OOP');

image.png

image.png

原型对象

  function Course() {}
  const course1 = new Course();
  const course2 = new Course();

** 构造函数:初化创对象的函数 Course
** 自动给构造函数赋属性prototype,等于实对象__proto__属性
** 实例对象:course1是实例对象,据原型创出的实例(每个对象都有__proto__和constructor)
** constructor由继承而来,并指向当前构造函数

继承

在原型对象的所有属性和方法,都能被实例所共享

// Game类
  function Game() {
    this.name = 'lol';
  }
  Game.prototype.getName = function() {
    return this.name;
  }
  // LOL类
  function LOL() {}
  // LOL继承Game类
  LOL.prototype = new Game();
  /**这句话的意义?根据图: 
  game要拿到game.name, 通过找game的__proto__找到LOL.prototype  LOL.prototype因为指向了new Game的实例,因此通过继承构造函数的this指向实例可以拿到name的值。又或者更深一步考虑,指向new Game,因此会去找new Game的__proto__,因此找到Game的prototype. Game的prototype没找到又找到了Game的构造函数,里面包含name,因此找到了name的值。 因此通过LOL.prototype = new Game(),重写了原型对象,将父对象的属性方法,作为子对象原型对象的属性和方法。**/
  LOL.prototype.constructor = LOL;
  const game = new LOL();
  // 本质: 重写原型对象,将父对象的属性方法,作为子对象原型对象的属性和方法

image.png

image.png

原型链继承

function Game() {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL() {}
// LOL继承Game类
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game1 = new LOL();
const game2 = new LOL();
game1.skin.push('ss');

此时game1.skin与game2.skin相同,都是['s','ss']

    1. 父属赋子类原型属性,此属于子共享属性,因此会互相影响。
    1. 实例化子类时,不能向父类传参

构造函数继承

在子类构造函数内部调用父类构造函数

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
  this.arg = arg;
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}

// LOL继承Game类
const game3 = new LOL(1);
const game4 = new LOL(2);
console.log(game3.arg, game4.arg);
game3.skin.push('1', '2');
console.log(game3.skin,game4.skin)
game3.getName();
//输出结果
1 2
['s', '1', '2'] ['s']
game3.getName is not a function
// 解决了共享属性问题&传参问题, 但是调用原型链上的方法时,显示not a function

原型链上的共享方法无法被读取继承,如何解决?

组合继承

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
  this.arg = arg;
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}
// LOL继承Game类
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game3 = new LOL();

组合继承就没有缺点么? 问题就在于:无论何种场景,都会调用两次父类构造函数。

    1. 初始化子类原型时
    1. 子类构造函数内部call父类的时候

寄生组合继承

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
}
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
LOL.prototype = Object.create(Game.prototype); 
LOL.prototype.constructor = LOL;
// LOL继承Game类
const game3 = new LOL();

提高:看起来完美解决了继承。js实现多重继承?

function Game(arg) {
  this.name = 'lol';
  this.skin = ['s'];
}
Game.prototype.getName = function() {
  return this.name;
}
function Store() {
  this.shop = 'steam';
}
Store.prototype.getPlatform = function() {
  return this.shop;
}
// LOL类
function LOL(arg) {
  Game.call(this, arg);
  Store.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
// LOL.prototype = Object.create(Store.prototype);
Object.assign(LOL.prototype, Store.prototype);
LOL.prototype.constructor = LOL;
// LOL继承Game类
const game3 = new LOL();

Object.create 和 new 有什么区别?

  • 构函Foo原属Foo.prototype指向了原型对象。
  • 原对保着实例共享方法,有指针constructor指回构造函数。
  • js中只函数有 prototype,对象只有__proto__ 隐式属性

Object.create

Object.create = function (o) {
    var F = function () {}
    F.prototype = o
    return new F()
}

Object.create是内部定义一个函数,并且让F.prototype对象 赋值为引进的对象/函数 o,并return出一个新的对象。

new

new是建个新对象o1,让o1__proto__指Base.prototype对象。并且使用 call 进行强转作用环境。实现实例创建。

区别

var Base = function () {
    this.a = 2
}
var o1 = new Base();
var o2 = Object.create(Base);
console.log(o1.a);// 2
console.log(o2.a) // undefined

Object.create()丢失了对原构造函数的属性访问

如何获取到一个实例对象的原型对象?

  • 从 构造函数 获得 原型对象:
构造函数.prototype
  • 从 对象实例 获得 父级原型对象
方法一: 对象实例.__proto__        【 有兼容性问题,不建议使用】
方法二:Object.getPrototypeOf( 对象实例 )

如何确保你的构造函数只能被new调用,而不能被普通调用?

1.使用 instanceof 实现

instanceof 用检构函 prototype 属是否出现在某实例对象原型链上。 检测某对象是不是另一对象实例,例如: new Person() instanceof Person --> true

2.new 绑定/ 默认绑定

  • new来调构造函数,会生一新对象,并把新对象绑定为调用函数的 this 。
  • 如普通调函,非严模this 指window,严模指undefined
function Test() {
    console.log(this)
}
console.log(Test()) // Window {}
console.log(new Test()) // Test {}

new 调和普调最大区别在函内this 指向不同:
new 调后 this 指实例,
普调指 window

instanceof 可检某对象是不是另一对象实例。
如为new 调this指向实例,this instanceof 构造函数 返回值为 true ,普调返false

image.png

new.target

若构函非new调,返 undefined可确定构函怎么调的

function Person () {
    console.log(new.target);
}
console.log('new:',  new Person()) // new: Person {}
console.log('not new:', Person()) // not new: undefined
复制代码

实现对构造函数限制

function Person () {
    if(!(new.target)) {
        throw new TypeError('Function constructor A cannot be invoked without "new"')
    }
}
// Uncaught TypeError: Function constructor A cannot be invoked without "new"
console.log('not new:', Person())

复制代码

使用ES6 Class

类也具备限构函只用new调的作用。
ES6 提供 Class 作构函语法糖,实语义化更好面向对象编程,并对 Class规定:类的构造器必须使用 new 来调用

后续面向对象编时,强推ES6 的 Class
Class修复了很多ES5缺陷,如类中所有方法不可枚举;类所有方法都无法被当构函使用等。

new.target 实现抽象类

image.png Class 内部调用 new.target,返回当前 Class需要注意的是,子类继承父类时,new.target会返回子类

image.png 通过子类调用和父类调用返结果不同,利用此特性,可实现父不可调而子可调情况——面向对象中的抽象类

抽象类实现

抽象类可理解为不能独立使用、需继承后

image.png抽象类Animal不能直接调,需继承后才能使用

image.png

this

不同环境下this指向

  1. 全局上下文
this等价于window
console.log(window === this); // true
var a = 1;this.b = 2;
window.c = 3;
console.log(a + b + c); // 6
  1. 函数上下文 this的值取决于函数被调用方式
  • 直接调用: 指向全局变量
function foo(){
 return this;
}
console.log(foo() === window); // true
  • call/apply: 指向绑定的对象上
var person = {
 name: "axuebin", age: 25
};
function say(job){
 console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25 FE
say.apply(person,["FE"]); // axuebin:25 FE
call和apply从this的绑定角度来说是一样的,唯一不同的是他们的第二个参数

bind: 将永久的绑定到bind的第一个参数

var person = {
  name: "axuebin",
  age: 25
};
function say(){
  console.log(this.name+":"+this.age);
}
var f = say.bind(person);
console.log(f());
  1. 作为对象的一个方法 指向调用函数的对象
var person = {
  name: "axuebin",
  getName: function(){
return this.name;
  }
}
console.log(person.getName()); // axuebin

var name = "xb";
var person = {
  name: "axuebin",
  getName: function(){
return this.name;
  }
}
var getName = person.getName;
console.log(getName()); // xb
this的指向得看函数调用时。
  1. 作为一个构造函数
绑定到正在构造的新对象上
function Person(name){
  this.name = name;
  this.age = 25;
  this.say = function(){
console.log(this.name + ":" + this.age);
  }
}
var person = new Person("axuebin");
console.log(person.name); // axuebin
person.say(); // axuebin:25
  1. 作为dom事件处理函数 指向事件所绑定的dom节点
var ele = document.getElementById("id");
ele.addEventListener("click",function(e){
  console.log(this);
  console.log(this === e.target); // true
})
  1. HTML标签内链事件处理函数 指向所在的DOM元素
<button onclick="console.log(this);">Click Me</button>
  1. Jquery下的this 指向dom的元素节点
$(".btn").on("click",function(){
  console.log(this); 
});
  1. 总结

判断this绑定,需调用位置。
由new调用:绑定到新创建的对象
由call或apply、bind调用:绑定到指定的对象
由上下文对象调用:绑定到上下文对象
默认:全局对象
注意:箭头函数this,继承外层函数this绑定。

上下文 + 作用域

执行上下文/执行栈

image.png

作用域

定变量的区域。定了当前执代对变访权。
var: 创变只函数作用域
let const:创变有函有块
分类:
静态作用域: 采词法作,就是静态作,函的作用域在函数定义时决定
动态作用域: 函作用域在函调时才决定。bash就是动态作用域\

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();
// 结果是 1
执行foo函数,在函数内部查找是否有变量value,因为没有查找到变量,就根据位置向上一层查找,所以value打印的值等于1

//假设js采用动作用域
执行foo函数,在函内部没有查找到变量value,就从下用foo函数的作用域,也就是bar作用域中查找value的值所以打印的值等于2

用命令行执行 bash ./scope.bash,看看打印的值是多少。
value=1
function foo () {
echo $value;
}
function bar () {
local value=2;
foo;
}
bar

作用域链

  1. 查找变量时,会从当前上下文变量中查找
  2. 没找到就会从父级执行上下文变量中查找
  3. 一直找到全局上下文变量对象,也就是全局对象
  4. 这样由多个执行上下文变量对象构成的链叫做作用域链

总结:每一个作用域都有对父级作用域的引用,当我们使用一个变量的时候,首先会在当前作用域中查找,找不到会沿着作用域链一直向上查找,直到全局作用域

let x0 = 0;
(function autorun1(){
 let x1 = 1;

 (function autorun2(){
   let x2 = 2;

   (function autorun3(){
 let x3 = 3;

         console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
    })();
  })();
})();

image.png

this上下文context

this是在执行时动态读取上下文决定的,不是在定义时决定

函数直接调用 - this 指向 window

  function foo() {
    console.log('函数内部的this:', this);
  }
  foo();

隐式绑定 - this指向调用堆栈的上一级

  function fn() {
    console.log(this)
    console.log('隐式绑定:', this.a);
  }
  const obj = {
    a: 1
  }

  obj.fn = fn;
  obj.fn();

输出: {a: 1, fn: ƒ} 隐式绑定: 1

实战:

  const foo = {
    bar: 10,
    fn: function() {
      console.log(this.bar);
      console.log(this);
    }
  }
  let fn1 = foo.fn;
  fn1();
  //输出: 
undefined
window


  // 追问,如何改变指向

  const o1 = {
    text: 'o1',
    fn: function() {
      console.log(this)
      return this.text;
    }
  }
  const o2 = {
    text: 'o2',
    fn: function() {
      return o1.fn();
    }
  }
  const o3 = {
    text: 'o3',
    fn: function() {
      let fn = o1.fn;
      return fn();
    }
  }

  console.log(o1.fn()); //{text: 'o1', fn: ƒ}  o1
  console.log(o2.fn()); //{text: 'o1', fn: ƒ}  o1
  console.log(o3.fn()); // Window undfined
    1. 在执行函数时,如果函数被上一级所调用,那么上下文即指向上一级
    1. 否则为全局孤立,指向window

追问: 现在我需要将console.log(o2.fn())结果是o2

// 1 - 人为干涉、改变this - bind/call/apply
// 2 - 不许改变this
const o1 = {
  text: 'o1',
  fn: function() {
    return this.text;
  }
}
const o2 = {
  text: 'o2',
  fn: o1.fn
}
// this指向最后调用他的对象,在fn执行时,函数挂到o2上即可
最终o2.fn()得到的结果就是'o2'

显式绑定(bind | apply | call)

  function foo() {
    console.log('函数内部的this:', this);
  }
  foo();

  foo.call({a: 1});
  foo.apply({a: 1});

  const bindFoo = foo.bind({a: 1});
  bindFoo();

追问:call、apply、bind的区别

相同点:

  1. 都是改变this指向的;
  2. 第一个参数都是this要指向的对象;
  3. 都可以利用后续参数传参; 不同点:
  4. 传参不同,call和bind的参数是依次传参的参数列表,逗号分隔,一一对应的
  5. 但apply只有两个参数,第二个参数为数组
  6. call和apply都是对函数进行直接调用,bind 是返回绑定 this 之后的函数
  7. bind()会返回一个新的函数,如果这个返回的新的函数作为构造函数创建一个新的对象,那么此时 this 不再指向传入给 bind 的第一个参数,而是指向用 new 创建的实例

new - this指向的是new之后得到的实例

  class Course {
    constructor(name) {
      this.name = name;
      console.log('构造函数中的this', this);
    }

    test() {
      console.log('类方法中的this', this);
    }
  }
  const course = new Course('this');
  course.test();
  
 // 构造函数中的this Course {name: 'this'}
 // 类方法中的this Course {name: 'this'}

追问: 异步方法中this有区别么

  class Course {
    constructor(name) {
      this.name = name;
      console.log('构造函数中的this', this);
    }

    test() {
      console.log('类方法中的this', this);
    }

    asyncTest() {
      console.log('异步方法外', this);
      // setTimeout(() => {
      //   console.log('异步方法中的this', this); // 改成箭头函数后this指向course 指向与外层作用域this相同 
      // }, 100)
      setTimeout(function() {
        console.log('异步方法中的this', this);
      }, 100)
    }
  }
  const course = new Course('this');
  course.test();
  course.asyncTest();

打印输出:

  1. 构造函数中的this Course {name: 'this'}
  2. 类方法中的this Course {name: 'this'}
  3. 异步方法外 Course {name: 'this'}
  4. undefined
  5. 异步方法中的this Window 
    1. 执行setTimeout时,传入匿名function执行,效果和全局执行函数效果相同
    1. 再追问,如何解决。把function改为无独立上下文的箭头函数即可

追问 bind原理 / 手写bind

    1. bind在哪里
  function sum(a, b, c) {
    console.log(a, b, c); //  2 3 undefined
    console.log('this', this); // Number {1}
    return a + b + c;
  }
  // 1. sum.bind - 在哪里 ? => Function.prototype
  Function.prototype.newBind = function () {
      // 2. bind 是什么?
      // a.返回一个函数 b. 返回原函数执行结果 c. 传参不变
      const _this = this;
      // args特点: 第一项 - newThis, 第二项 ~ 最后一项 - 函数传参
      const args = Array.prototype.slice.call(arguments);
      const newThis = args.shift();
      console.log('newThis', newThis);// newThis 1
      return function () {
          return _this.apply(newThis, args);
      }
  }
  sum.newBind(1,2,3)()
    1. apply应用 - 多传参数组化

  const arr = [2, 4, 5, 6];
  let max = Math.max.apply(this, arr); // 6 this换成{}都可,相当于apply的副业
  效果相当于:
  Math.max(2, 4, 5, 6); // 6

优先级 - new > 显式 > 隐式 > 默认

  function fn() {
    console.log(this);
  }
  const obj = {
    fn
  }
  obj.fn() // obj 隐式绑定
  
 // 显式 > 隐式
 obj.fn.bind(111)() // Number {111} call,apply, bind为显示
 
function foo(a) {
    this.a = a
}
const obj1 = {}
let bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2  foo里的this指向的是obj1

// new
let baz = new bar(3)

// new > 显式
console.log(obj1.a) //2
console.log(baz.a) //3

箭头函数的 this 指向哪⾥?

箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的this值

// ES6 
const obj = {
    getArrow() { 
        return () => { 
            console.log(this === obj); // true
        }; 
    } 
}

bind() 连续调用多次,this的绑定值是什么呢?

image.png 答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。 原因是,在Javascript中,多次 bind() 是无效的。

以下代码的输出是什么?

image.png

window
obj
window
window

聊完了作用域和上下文,如何突破作用域的束缚

闭包: 一个函数和他周围状态的引用捆绑在一起的组合

闭包

是可访外部作用域的内部函数,即使外部作用域已经执行结束。被引用变量直到闭包被毁才销毁

闭包的几种类型:

1. 嵌套作用域

js中函数里面嵌套函数

(function autorun(){
    let x = 1;
    function log(){ 
       console.log(x); 
    }
    log();
})();

  let counter = 0;

  function outerFn() {
    function innerFn() {
      counter++;
      console.log(counter);
    }
    return innerFn;
  }
outerFn()() // 1

log函数可以访问外部函数作用域的变量的值x

2. 外部函数作用域

内函可访外函作域中变量值,即使外部函数已执行结束

(function autorun(){
    let x = 1;
    setTimeout(function log(){
          console.log(x);//1
    }, 10000);
})();

内函还可访问外函形参

(function autorun(p){
    let x = 1;
    setTimeout(function log(){
      console.log(x);//1
      console.log(p);//10
    }, 10000);
})(10);

3. 外块级作用域

内函可访外部块作域定变量,即使外部块已执完

{
    let x = 1;
    setTimeout(function log(){
      console.log(x);
    }, 10000);
}

4. 词法作用域

词法作用域指内部函数在定义的时候就已经决定了其作用域为外部作用域

(function autorun(){
    let x = 1;
    function log(){
      console.log(x);
    };

    function run(fn){
      let x = 100;
      fn();
    }

    run(log);//1
})();

 let content;
  function mail() {
    console.log(content);
  }
  function envelop(fn) {
    content = 1; // 其实修改的是外部作用于content的值

    fn();
  }
  envelop(mail); // 1

log函数的作用域在定义的时候就已经决定了其作用域为外部作用域。 autorun的函数作用域就是log函数的词法作用域

函数作为返回值场景

  function mail() {
    let content = '信';
    return function() {
      console.log(content);
    }
  }
  const envelop = mail();
  envelop();
  • 函数外部获取到了函数作用域内的变量值

追问:

立即执行嵌套

  (function immediateA(a) {
    return (function immediateB(b) {
      console.log(a, b); // 0 1
    })(1);
  })(0);

立即执行遇上块级作用域 !!!!!!!!!

  let count = 0;
  (function immediate() {
    if (count === 0) {
      let count = 1;

      console.log(count); // 1
    }
    console.log(count); // 0
  })();

拆分执行 多个闭包

  function createIncrement() {
    let count = 0;

    function increment() {
      count++;
      console.log(count);
    }

    let message = `count is ${count}`;

    function log() {
      console.log(message);
    }

    return [increment, log];
  }

  const [increment, log] = createIncrement();
  increment(); // 1
  increment(); // 2
  increment(); // 3
  log(); // count is 0
  

外部作用域执行完毕后,内部函数还存活时,闭包才真正发挥作用

定时器

(function autorun(){   
let x = 1;
setTimeout(function log(){
  console.log(x);
}, 10000);
})();
x 将一直存活着直到定时器的回调执行或者 clearTimeout() 被调用.\
如果这里使用的是 setInterval() ,那么变量 x 将一直存活到 clearInterval() 被调用。

事件处理

(function autorun(){
let x = 1;
$("#btn").on("click", function log(){
  console.log(x);
});
})();
当变量 x 在事件处理函数中被使用时,它将一直存活直到该事件处理函数被移除

异步

(function autorun(){
let x = 1;
fetch("http://").then(
function log(){
  console.log(x);
});
})();

变量 x 将一直存活到接收到后端返回结果,回调函数被执行

含有循环语句的事件处理(异步执行)的闭包

  let lis = document.getElementsByTagName('li');
 
  for (var i = 0; i< lis.length; i++) {
      lis[i].onclick = function() {
        console.log(i);
      }
      setTimeout(function() {
        console.log(i); // 6个6 因为上层作用域的变量i已经循环完成
      }, 100)
  }
  
  for (var i = 0; i< lis.length; i++) {
    (function(i) {   
      lis[i].onclick = function() {
        console.log(i);
      }
      setTimeout(function() {
        console.log(i); // 1,2,3,4,5 拿到的是闭包声明的时候的作用域的i,这时候已经传进来了,不会被外层循环所累加
      }, 100)
    })(i);
  }

变量生命周期

变量生命周期取决于闭包生命周期。被闭包引的外部作用域中变量将一直存活到闭包函数被销毁。
如果一个外部变量被多个闭包引用,那么直到所有闭包被垃圾回收后,该变量才会被销毁。

闭包和封装性

函数与私有状态
通过闭包,我们可以创建拥有私有状态的函数,闭包使得状态被封装起来。

闭包和纯函数

闭包是使用了外部作用域中变量的函数
纯函数是没有使用外部作用域中变量的函数,它通常返回一个值且无副作用

闭包习题训练

juejin.cn/post/684490…

深拷贝和浅拷贝的区别?怎样实现深拷贝?

数据类型存储
JavaScript中存在两大数据类型:
基本类型: 数据在栈内存中
引用类型: 数据在堆内存中,变量是一个指向堆内存中实际对象的引用,存在栈中

浅拷贝

定义:创建新数据,此数据原始数据的精确拷贝。
如属性是基础数据类型,则拷基本数据的值,引用数据类型,拷贝的是内存地址。
即浅拷贝只拷贝一层

image.png

image.png

object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别是什么?

  • 扩展运算符
let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
  • Object.assign()
let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

可以看到,两者都是浅拷贝。

深拷贝

_.cloneDeep()

image.png

jQuery.extend()
image.png

JSON.stringify()
image.png

区别:
浅拷和深拷都创一个新对象,但复制属性时,行为不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

image.png 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

image.png 小结:
前提为拷贝类型为引用类型的情况下:
浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

es5 中的class和es6中的class有什么区别?

在es5中主要是通过构造函数方式和原型方式来定义一个类,在es6中我们可以通过class来定义类。

一、class类必须new调用,不能直接执行。

image.png

而es5中的类和普通函数并没有本质区别,执行肯定是ok的。

二、class类不存在变量提升

es5中的类 image.png

es6中的类 image.png 图2报错,说明class方式没有把类的定义提升到顶部。

三、class类无法遍历它实例原型链上的属性和方法

function Foo (color) {
  this.color = color
}
Foo.prototype.like = function () {
  console.log(`like${this.color}`)
}
let foo = new Foo()

for (let key in foo) {
  // 原型上的like也被打印出来了
  console.log(key)  // color、like
}

class Foo {
  constructor (color) {
      this.color = color
  }
  like () {
      console.log(`like${this.color}`)
  }
}
let foo = new Foo('red')
foo.like() // like red
for (let key in foo) {
  // 只打印一个color,**没有打印like**
  console.log(key)  // color
}

四、new.target属性

es6为new命令引入了一个new.target属性,返回new作用于的那个构函。

如果不是通过new调,new.target会返回undefined

image.png

image.png

五、class类有static静态方法

static静态方法只能通过类调用,不会出现在实例上
静态方法中this指的是类,不是实例
static声明的静态属性和方法都可以被子类继承。

class Foo {
  static bar() {
    console.log(`this ${this}`)
    this.baz(); // 此处的this指向类
  }
  static baz() { // 静态方法
    console.log('hello'); // 不会出现在实例中
  }
  baz() { // 原型方法
    console.log('world');
  }
}
let foo = new Foo();
foo.bar() // undefined
foo.baz() // world
Foo.bar() // this Foo hello

for (let key in foo) { console.log(key) } // 打印为空,说明静态static方法以及原型方法都遍历不到,如果在constructor中添加this.a 可以遍历出a属性以及值。constructor 方法是类的默认方法,创建类的实例化对象时被调用

ajax/axios/fetch

ajax原理

image.png

ajax、axios、fetch有什么区别?

(1)AJAX

Ajax交互式网页应用技术。无需重载整个网页情况下,更新部分网页。通过后台与服务器进行少量数据交换,可使网页异步更新。缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 配置和调用方式混乱,基于事件的异步模型不友好。

(2)Fetch

AJAX的替代品,基于ES6 promise设计的。代码结构比起ajax简单。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。 fetch的优点:

  • 语法简洁,更加语义化

  • 基于标准 Promise 实现,支持 async/await

  • 更加底层,提供的API丰富(request, response)

  • 脱离了XHR,是ES规范里新的实现方式 fetch的缺点:

  • 对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

  • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})

  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费

  • fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios

Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

  • 浏览器中创建XMLHttpRequests
  • 基于promise的异步ajax请求库
  • 浏览器端 node端都可以使用
  • 支持Promise API
  • 支持请求/响应拦截器,支持请求取消
  • 对请求和返回进行转化
  • 批量发送多个请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

正则表达式

构建正则表达式有两种方式

1.  字面量创建,其由包含在斜杠之间的模式组成
const re = /\d+/g;

2.  调用`RegExp`对象的构造函数
const re = new RegExp("\\d+","g"); 
const rul = "\\d+" 
const re1 = new RegExp(rul,"g");

二、匹配规则

规则描述
\转义
匹配输入的开始
$匹配输入的结束
*匹配前一个表达式 0 次或多次
+匹配前面一个表达式 1 次或者多次。等价于 {1,}
?匹配前面一个表达式 0 次或者 1 次。等价于{0,1}
.默认匹配除换行符之外的任何单个字符
x(?=y)匹配'x'仅仅当'x'后面跟着'y'。这种叫做先行断言
(?<=y)x匹配'x'仅当'x'前面是'y'.这种叫做后行断言
x(?!y)仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找
(?<!y)x仅仅当'x'前面不是'y'时匹配'x',这被称为反向否定查找
xy匹配‘x’或者‘y’
{n}n 是一个正整数,匹配了前面一个字符刚好出现了 n 次
{n,}n是一个正整数,匹配前一个字符至少出现了n次
{n,m}n 和 m 都是整数。匹配前面的字符至少n次,最多m次
[xyz]一个字符集合。匹配方括号中的任意字符
[^xyz]匹配任何没有包含在方括号中的字符
\b匹配一个词的边界,例如在字母和空格之间
\B匹配一个非单词边界
\d匹配一个数字
\D匹配一个非数字字符
\f匹配一个换页符
\n匹配一个换行符
\r匹配一个回车符
\s匹配一个空白字符,包括空格、制表符、换页符和换行符
\S匹配一个非空白字符
\w匹配一个单字字符(字母、数字或者下划线)
\W匹配一个非单字字符

正则表达式标记

标志描述
g全局搜索。
i不区分大小写搜索。
m多行搜索。
s允许 . 匹配换行符。
u使用unicode码的模式进行匹配。
y执行“粘性(sticky)”搜索,匹配从目标字符串的当前位置开始。

贪婪模式

const reg = /ab{1,3}c/   // abc, abbc, abbbc

const string = "12345";
const regx = /(\d{1,3})(\d{1,3})/;
console.log( string.match(reg) ); 
// => ["12345", "123", "45", index: 0, input: "12345"]
// 前面的`\d{1,3}`匹配的是"123",后面的`\d{1,3}`匹配的是"45"

懒惰模式

惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配

var string = "12345";
var regex = /(\d{1,3}?)(\d{1,3})/;
console.log( string.match(regex) );
// => ["1234", "1", "234", index: 0, input: "12345"]
// 其中`\d{1,3}?`只匹配到一个字符"1",而后面的`\d{1,3}`匹配了"234"

分组

分组主要是用过()进行实现
beyond{3},是匹配d字母3次
(beyond){3}是匹配beyond三次

()内使用|达到或的效果
(abc | xxx)可以匹配abc或者xxx
反向引用,巧用$分组捕获

let str = "John Smith";

// 交换名字和姓氏
console.log(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

三、匹配方法

  • 字符串(str)方法:matchmatchAllsearchreplacesplit

  • 正则对象下(regexp)的方法:testexec

方法描述
exec一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。
test一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。
match一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。
matchAll一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。
search一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
split一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

应用场景

验证QQ合法性(5~15位、全是数字、不以0开头):

const reg = /^[1-9][0-9]{4,14}$/ 

校验用户账号合法性(只能输入5-20个以字母开头、可带数字、“_”、“.”的字串):

var patrn=/^[a-zA-Z]{1}([a-zA-Z0-9]|[._]){4,19}$/; 
const isvalid = patrn.exec(s)

npm babel JSBridge

npm 是什么?

image.png

Babel 是什么?

image.png

JSBridge是什么?

image.png

数据/typeof / instanceof /Object.is/===/==

数据类型检测的方式有哪些?

(1)typeof

image.png 其中数组、对象、null都会被判断为object,其他判断都正确。
typeof不能用来判断null和数组,结果都是Object

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

image.png 可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型

(3)constructor

image.png constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了

image.png

(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型: image.png

image.png 这个基本可以判断所有的数据类型

NaN、isNaN 和 Number.isNaN相关题目

NaN 是什么,用 typeof 会输出什么?

NaN:Not a Number,表示非数字

typeof NaN === 'number' // true

NaN 是一个特殊值,它和自身不相等

NaN == NaN // false
NaN === NaN // false
NaN !== NaN // true

/** NaN更多的是表示不是一个数值的状态,而不是一个数值的状态有很多种情况,所以互不相等。 **/

isNaN 和 Number.isNaN 函数有什么区别?

NaN

NaN更多的是表示不是一个数值的状态,而不是一个数值的状态有很多种情况,所以互不相等。

方法

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,不能被转换为数值的返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

instanceof能否判断基本数据类型?

能。 image.png

null是对象吗?为什么?

null不是对象。 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。

null 和 undefined 有什么区别?

Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null.
undefined 代表的含义是未定义,null 代表的含义是空对象. 一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

Object.is与===和==有什么区别?

  • == 双等号判等,会在比较时进行[类型转换]
  1. 如操作数都是对象,仅当两个操作数都引用同一个对象时才返回true。
const obj = {name:"张三", age:20}
const obj2 = obj
console.log({name:"张三", age:20} == obj)  //false
console.log(obj2 == obj) //true
  1. 两个操作数不同类型,会在比较前进行类型转换,将其转换为同的类型,然后再比较它们。 转换过程遵循以下规则(其实都是调用valueOf和toString方法)

如果一个操作数是布尔值,则将布尔值转化为数值0或1 如果是字符串和数值比较,则将字符串转化为数值 如果是对象和其余类型比较,则调用对象valueOf()方法


console.log("1221" == 1221)   //true
console.log(true == 1)   //true
console.log(false == 0)  //true
console.log(Number(10) == 10) // true
console.log(+0 == -0)   //true
console.log(NaN == NaN)   //false

console.log(null == false); //false `因为null和undefined根本就没有valueOf方法.`
console.log(null == 0); //false
console.log(undefined == false); //false
console.log(undefined == 0); //false
console.log(undefined == null); //true 是因为在语言设计时,设计者觉得二者很像,才使他们相等

console.log(null.valueOf()); //Cannot read property 'valueOf' of null
console.log(undefined.valueof()); //Cannot read property 'valueOf' of null

习题

'' == '0' // false 因为都是string的比较,所以不进行转化
0 == '' //  Number('')->0
0 == ‘0’ // Number('0')->0
false == 'false' // Boolean('false')-> true
false == '0' // Number(false)->0 '0'-> 0 
!!!!!!!!!!!
  • === 比较时不进行隐式类型转化,类型不同则返回false。
console.log("12" === 12)   //false
console.log(null === null)   //true
console.log(undefined === undefined) //true
console.log(NaN === NaN)   //false
console.log(+0 === -0)  //true

补充:

Boolean('0') // true
Boolean(0) // false
Boolean('') // false
'' == '0' // false
0 == '' // true
0 == '0' // true
  • Object.is()

Object.is()和三等运算符很像,但是仍然存在一些差别,比如说对于NaN,对于+0和-0之间的比较

console.log(Object.is(10, 10)) //true
console.log(Object.is(NaN, NaN)) //true           !!
console.log(Object.is({}, {})) // false
console.log(Object.is(12, "12")) //false
console.log(Object.is(true, 1)) //false
console.log(Object.is(false, false)) //true
console.log(Object.is(+0, -0)) //false             !!
console.log(Object.is(undefined, null))  //false

满足如下条件的Object.is()的返回值为true.

1.都为undefined
2.都是null
3.都是true或者都是false.
4.都是相同长度的字符串并且相同字符换按照相同的顺序排列
5.都是相同的对象(同一个引用)
6.都是数字,且大都是+0,或者都是-0,或者都是NaN或者都是非零而且非NaN且为同一个值

  • 总结
  1. ==需要做类型转化,Object.is()不需要
  2. ===下,+0-0相同,NaNNaN不同,在Object.is()下,+0和-0不相同,NaN和NaN相同。

[] == ![]结果是什么?

== 中,左右两边都需要转换为数字然后进行比较。 []转换为数字为0。 ![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为true, 因此![]为false,进而在转换成数字,变为0。 0 == 0 , 结果为true

'1'.toString()为什么不会报错?

在这个语句运行的过程中做了这样几件事情:

image.png

  • 第一步: 创建Object类实例。
  • 第二步: 调用实例方法。
  • 第三步: 执行完方法立即销毁这个实例。 结果还是字符串'1'

0.1+0.2为什么不等于0.3

浮点数转二进制时丢失了精度,计算完再转回十进制时出现的问题

什么是BigInt

BigInt是一种新的数据类型,当整数值大于Number数据类型支持范围,这种数据类型允许我们安全地对大整数执行算术操作。

改造下面的代码,让它输出1,2,3,4,5

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
    	console.log(j)
    }, 0)
  })(i)
}

for(var i=1;i<=5;i++){
  setTimeout(function timer(j){
  	console.log(j)
  }, 0, i)
}
for(let i = 1; i <= 5; i++){
  setTimeout(function timer(){
 	 console.log(i)
  },0)
}

letJS有函数作用域变为了块级作用域,用let后作用域链不复存在。

----------> 要移到ES6事件循环机制那里

下面执行后输出什么?

for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
  	console.log(i)
  }, 0)
}

结论: 输出5个6。

因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任 务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

如何判断一个对象是不是空对象?


Object.keys(obj).length === 0 
JSON.stringify(obj) === '{}'

js中如何判断一个值是否是数组类型?

instanceof, Array.isArray

const arr= []; 
arr instanceof Array; // true
Array.isArray(arr) // true
const obj = {}
Array.isArray(obj) // false

Object.prototype.isPrototypeOf

方法用于测试一个对象是否存在于另一个对象的原型链上。

Object.prototype.isPrototypeOf(arr, Array.prototype); // true

Object.getPrototypeOf

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)

Object.getPrototypeOf(arr) === Array.prototype// true

Object.prototype.toString.call

Object.prototype.toString.call(arr) === '[object Array]' // true
const obj = {} 
Object.prototype.toString.call(obj) // "[object Object]"

如何区分数组和对象?

1. 通过 ES6 中的 Array.isArray 来识别

console.log(Array.isArray([]))//true
console.log(Array.isArray({}))//false

2. 通过 instanceof 来识别

console.log([] instanceof Array)//true
console.log({} instanceof Array)//false

3. 通过调用 constructor 来识别

console.log([].constructor)//[Function: Array]
console.log({}.constructor)//[Function: Object]

4. 通过 Object.prototype.toString.call 方法来识别

console.log(Object.prototype.toString.call([]))//[object Array]  
console.log(Object.prototype.toString.call({}))//[object Object]   

const声明了数组,还能push元素吗,为什么?

可以
数组是引用类型,const声明的引用类型变量,不可以变的是变量引用始终指向某个对象,不能指向其他对象,但是所指向的某个对象本身是可以变的

将数组的length设置为0,取第一个元素会返回什么?

设置 length = 0 会清空数组,所以会返回 undefined

循环/枚举/递归/尾调用/尾递归/函数式编程/函数缓存/异步编程

Object与Map有什么区别?

  • Object

Object是一个顶级对象,还是一个构造函数,通过它(如:new Object())来创建对象。可以认为JS中所有对象都是Object的实例.

  • Map

MapObject的一个子类,可有序保存任意类型的数据,使用键值对去存储,其中键可以存储任意类型,通过const m = new Map(),即可得到一个map实例。

访问: map.get(key)
赋值: map.set
删除: 通过map.delete去删除一个值,不存在属性返false
length: map.size
迭代:for-of forEach

  • Object

访问:obj.a或者obj['a']
赋值: object.a = 1或者object['a'] = 1,key只能是字符串,数字或symbol
删:delete. 但对象不存在该属性,删除也返回true,当然可以通过Reflect.deleteProperty(target, prop)  删除不存在的属性还是会返回true。

var obj = {}; // undefined
delete obj.a // true

length:Object.keys将其转换为数组,再通过length方法或使用Reflect.ownKeys(obj)也可以获取到keys的集合
迭代:for-in

使用场景

  1. 只需简单key-value数据,key不需存储复杂类型,用对象
  2. 对象须通过JSON转换,则只能用对象,暂不支持Map
  3. map阅读性好,操作都通过api去调,更有编程体验

forEach中return有效果吗?如何中断forEach循环?

在forEach中用return不会返回,函数会继续执行。

中断方法

  • 使用try监视代码块,在需要中断的地方抛出异常。
  • 官方推荐方法(替换方法):用every和some替代forEach函数。
    • every在碰到return false的时候,中止循环。
    • some在碰到return true的时候,中止循环。

for...in和for...of有什么区别?

for…of 是ES6新增的遍历方式,和for…in的区别如下:

  • for…of遍历获取键值,for…in获取键名
  • for…of不遍历原型链,for…in会遍历对象整个原型链,性能差
  • 对于数组遍历,for…in 返回数组所有可枚举属性(包括原型链上可枚举属性),for…of只返回数组下标对应属性值;

总结:

  • for...in 循环为遍历对象而生,不适用于遍历数组
  • for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

js对象中,可枚举性(enumerable)是什么?

可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for...in循环之中(除非属性名是一个Symbol)。如一个属性的enumerable为false,下面三个操作不会取到该属性。
for..in循环
Object.keys方法
JSON.stringify方法

var o = { a: 1, b: 2 };
o.c = 3;
Object.defineProperty(o, "d", {
  value: 4,
  enumerable: false,
});

o.d;
// 4

for (var key in o) console.log(o[key]);// 包含原型链属性
// 1
// 2
// 3

Object.keys(o); // ["a", "b", "c"] // 不包含原型链属性

JSON.stringify(o); // => "{a:1,b:2,c:3}"

Object.getOwnPropertyNames(o) // ['a', 'b', 'c', 'd'] 
// 如需获取对象自身所有属性,不管enumerable的值,可以使用`Object.getOwnPropertyNames`方法。

尾调用

某函数的最后一步是调用另一个函数。

function f(x) {
    return g(x)
} // 尾调用

function f(x) {
  if (x > 0) {
    return m(x) // 尾调用
  }
  return n(x); // 尾调用
}

function f(x) {
    let y = g(x)
    return y; //非尾调用
}

function f(x) {
    return g(x) + 1 //非尾调用
} 

上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

尾调用优化

尾调用是在函数的最后一步调用其他函数。与其他调用不同之处就在于它特殊的调用位置。
函数调用会在内存形成一个"调用记录"保存调用位置和内部变量等信息。
如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回,A,B的调用记录才会消失如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,直接用内层函数的调用记录,取代外层函数的调用记录

function f() {
    let m = 1
    let n = 2
    return g(m+n)
}
f();
//等同于
function f(){
    return g(3)
}
//等同于
g(3)

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。(如果调用后面还有其他操作,也就是不属于尾调用,那么外层函数的调用记录没法删除,同时要包含外层函数以及内层函数的调用记录会造成栈溢出
这就叫做"尾调用优化",即只保留内层函数的调用记录。可以做到每次执行时,调用记录只有一项,大大节省内存

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。节省内存

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。


function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

factorial(4, 5*1)
factorial(3, 4*5*1)
factorial(2, 3*4*5*1)
factorial(1, 2*3*4*5*1) -> return 2*3*4*5*1

数组求和

function sumArray(arr, total = 0) {
  if(arr.length === 0) {
      return total
  }
  return sumArray(arr, total + arr.pop())
}
let arr = [1,2,3,4,5]
sumArray(arr)
sumArray(arr, 0+5) 
sumArray(arr, 0+5+4) 
sumArray(arr, 0+5+4+3) 
sumArray(arr, 0+5+4+3+2) 
sumArray(arr, 0+5+4+3+2+1) 

使用尾递归优化求斐波那契数列

function fn(n) {
  if(n <= 2){
    return 1
  }
    //根据斐波那契数列递推的方法定义F(n)=F(n - 1)+F(n - 2)
    return f(n-1)+f(n-2);
}

fn(5)
fn(4)+fn(3)
fn(3)+1+1+1
1+1+1+1+1 = 5

数组扁平化

let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
    arr.forEach(v => {
        if(Array.isArray(v)) {
            result = result.concat(flat(v, []))
        }else {
            result.push(v)
        }
    })
    return result
}

数组对象格式化


let obj = {
  a: '1',
  b: {
      c: '2',
      D: {
          E: '3'
      }
  }
}
// 转化为如下:
let obj = {
  a: '1',
  b: {
      c: '2',
      d: {
          e: '3'
      }
  }
}

// 代码实现
function keysLower(obj) {
  let reg = new RegExp("([A-Z]+)", "g");
  for (let key in obj) {
      if (obj.hasOwnProperty(key)) { // 判断对象是否包含特定的自身(非继承)属性。
          let temp = obj[key];
          if (reg.test(key.toString())) {
              temp = obj[key.replace(reg, function (result) {
                  return result.toLowerCase()
              })] = obj[key];
              // 将之前大写的键属性删除
              delete obj[key];
          }
          // 如果属性值是对象或者数组,重新执行函数
          if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
              keysLower(temp);
          }
      }
  }
  return obj;
};

函数式编程

函数式编程是一种"编程范式"
主要的编程范式有三种:命令式编程,声明式编程和函数式编程
函数式编程更加强调执行结果非过程,把过程逻辑写成函数,定义好输入参数,只关心它的输出结果

image.png

纯函数:

纯函数=无状态+数据不可变
特性:
函数内部传入指定的值,就会返回确定唯一的值
不修改全局变量或引用参数
优势:
不依外部环境,不产副作用,提高复用性
模块化概念及单一职责原则

高阶函数:

函数作为输入或者输出的函数
存在缓存特性,主要是利用闭包

柯里化:

柯里化是把一个多参数函数转成一个单一参数的函数

1. 二元函数转化成柯里化函数

let fn = (x,y)=>x+y 

const curry = function (fn) {
    return function (x) {
        return function (y) {
            return fn(x,y)
        }
    }
}
let myfn = curry(fn)
console.log(myfn(1)(2))

结果为3

2. 多参数柯里化
const curry = function (fn) {
    return function curriedFn(...args) {
        if(args.length < fn.length) {
            return function () {
                return curriedFn(...args.concat([...arguments]))
            }
        }
        return fn(...args)
    }
}
const fn = (x,y,z,a) => x+y+z+a
const myfn = curry(fn)
console.log(myfn(1)(2)(3)(1))
// 结果7

异步编程有哪些实现方式?

  1. 回调函数 缺点是多个回调函数嵌套的时候会造成回调函数地狱,耦合度高,不利于维护。
  2. Promise 可将嵌套的回调函数作为链式调用。但多个then的链式调用造成语义不明确。
  3. async async函数内执行到await语句时,若语句返promise对象,函数将等promise resolve 后再继向下执行。可将异步逻辑转为同步顺序书写。

什么是提升(Hosting)

js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端

console.log(a);
var a = 3;
//预编译后的代码结构可以看做如下
var a; // 将变量a的声明提升至最顶端,赋值逻辑不提升。
console.log(a); // undefined
a = 3; // 代码执行到原位置即执行原赋值逻辑

什么是变量提升

函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。

在创建执行上下文的过程中(预编译)会做三件事:
创建变量对象
创建作用域链
确定this指向
创建变量对象过程中,扫码function函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。

foo();
var foo;
function foo(){ // 函数声明
  console.log(1);
}
foo = function(){ // 函数表达式
  console.log(2);
}

答案: 1 引擎会在执行JS代码前进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。
简单说就是在js代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端。而且函数提升只会提升函数声明,而不会提升函数表达式 image.png 输出undefined就很明了。

let和const 声明的变量和函数是不存在提升现象

函数提升

较之变量提升,函数的提升还是有区别的

image.png 函数提升只会提升函数声明,而不会提升函数表达式

再举一个小例子:

var a = 1; // 定义一个全局变量 a
function foo() {
    var a = function () {}; // 定义局部变量 a 并赋值。
    a = 10; // 修改局部变量 a 的值,并不会影响全局变量 a
    console.log(a); // 打印局部变量 a 的值:10
    return;
}
foo();
console.log(a); // 打印全局变量 a 的值:1

10
1

JS 小知识点

JS代码中的use strict是什么意思?

ECMAscript5严格运行模式 设立"严格模式"的目的:

  • 消除不合理、不严谨之处,减少一些怪异行为;消除代码运行不安全之处;
  • 提编效,增运速;
  • 未来新版本的Javascript 做铺垫

区别:

  • 禁止使用with。
  • 禁止this指向全局对象。
  • 对象不能有重名的属性。

为什么部分请求中,参数需要使用 encodeURIComponent 进行转码?

URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。
如URL中有汉字,就必须编码后使用不同操作系统、不同浏览器将导致完全不同的编码结果
就是使用Javascript先对URL编码,再向服务器提交,不要给浏览器插手的机会。输出总是一致的,保证了服务器得到的数据格式统一。 它对应的解码函数是decodeURIComponent()。

base64编码图片,为什么会让数据量变大?

Base64编码的思想是是采用64个基本的ASCII码字符对数据进行重新编码。把一个3字节为一组的数据重新编码成了4个字节。即字节数据量相应变大.

123['toString'].length + 123 的输出值是多少?

124

function的length

function fn1 (name) {} // fn1.length 1

function fn2 (name = '林三心') {} // fn2.length 0

function fn3 (name, age = 22) {} // fn3.length 1

function fn4 (name, gender, age = 22) {} // fn4.length 2

function fn5(name = '林三心', age, gender) { } // f5.length 0

function的length指的是第一个具有默认值之前的参数的个数

剩余参数不算入length之中

function fn1(name, ...args) {}

console.log(fn1.length) // 1

所以,123['toString'].length + 123 = ?

124

使用js生成1-10000的数组

实现的方法很多,除了使用循环(for,while,forEach等)外,最简单的是使用Array.from

// 方法一
Array.from(new Array(10001).keys()).slice(1) 
// 方法二 
Array.from({length: 1000}, (node, i) => i+1)

写一个 repeat 方法,实现字符串的复制拼接

方法一

空数组 join

function repeat(target, n) {
    return (new Array(n+1)).join(target)
}

方法二

改良方法1,省去创建数组这一步,提高性能。之所以创建一个带 length 属性的对象,是因为要调用数组的原型方法,需要指定 call 第一个参数为类数组对象。

function repeat(target, n) {
    return Array.prototype.join.call({
        length: n+1
    }, target)
}

使用原生js给一个按钮绑定两个onclick事件

image.png

document.write和innerHTML有什么区别

  • document.write直接写入到页面的内容流,会导致页面被重写。如果在写之前没有调用document.open, 浏览器会自动调用open。
  • innerHTML将内容写入某个DOM节点,不会导致页面全部重绘
  • innerHTML很多情况下都优于document.write,允许更精确的控制要刷新页面的那一个部分。

JavaScript中的错误有哪几种类型?

image.png

js中的undefined和 ReferenceError: xxx is not defined 有什么区别?

  • ReferenceError:未定义的变量/函数,或者变量找不到
  • undefined:当一个变量声明后,没有被赋值,那么它就是undefined类型。

如何判断当前脚本运行在浏览器还是 node 环境中?

this === window ? 'browser' : 'node'; 通过判断 Global 对象是否为 window,如果不为 window,当前脚本没有运行在浏览器中。

移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个延时?

移动端点击有 300ms 的延迟是因为移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,看用户有没有下一次点击,来判断这次操作是不是双击。 有三种办法来解决这个问题:

  • 通过 meta 标签禁用网页的缩放。
  • 通过 meta 标签将网页的 viewport 设置为 ideal viewport。
  • 调用一些 js 库,比如 FastClick

什么是点击穿透,怎么解决?

在发生触摸动作约300ms之后,它底下的具有点击特性的元素也会被触发,这种现象称为点击穿透。

常见场景

  1. 情景一:蒙层点击穿透问题,点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的click事件。
  2. 情二:另一种跨页面点击穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了。 发生的条件
  • 上层元素监听了触摸事件,触摸之后该层元素消失
  • 下层元素具有点击特性(监听了click事件或默认的特性(a标签、input、button标签)) 解决点击穿透的方法
  1. 方法一:书写规范问题,不要混用touch和click。既然touch之后300ms会触发click,只用touch或者只用click就自然不会存在问题了。
  2. 方法二:tap后延迟350毫秒再隐藏mask。

前端路由

什么是前端路由?

前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,之前是通过服务端根据 url 的不同返回不同的页面实现的。

什么时候使用前端路由?

在单页面应用,大部分页面结构不变,只改变部分内容的使用

前端路由有什么优点和缺点?

优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户 缺点:单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置

实现方式

前端路由一共有两种实现方式,一种是通过 hash 的方式,一种是通过使用 pushState 的方式。

怎么检测浏览器版本?

  1. window.navigator.userAgent:但这种方式很不可靠,因为 userAgent 可以被改写,并且早期的浏览器如 ie,会通过伪装自己的 userAgent 的值为 Mozilla 来躲过服务器的检测。

  2. 第二种方式是功能检测,根据每个浏览器独有的特性来进行判断,如 ie 下独有的 ActiveXObject

什么是 Polyfill ?

Polyfill 指的是用于实现浏览器并不支持的API 的代码。 比如说 querySelectorAll 是很多现代浏览器都支持的原生 Web API,但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就可以成为一个 Polyfill。

Js 动画与 CSS 动画区别及相应实现

  • CSS3 的动画的优点

    • 在性能上会稍微好一些,浏览器会对 CSS3 的动画做一些优化
    • 代码相对简单
  • 缺点

    • 在动画控制上不够灵活
    • 兼容性不好 JavaScript 的动画正好弥补了这两个缺点,控制能力很强,可以单帧的控制、变换,同时写得好完全可以兼容 IE6,并且功能强大。对于一些复杂控制的动画,使用 javascript 会比较靠谱。而在实现一些小的交互动效的时候,就多考虑考虑 CSS 吧

mouseover 和 mouseenter 有什么区别?

当鼠标移动到元素上时就会触发 mouseenter 事件,类似 mouseover,它们两者之间的差别是 mouseenter 不会冒泡。 由于 mouseenter 不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其 mouseover 和 mouseout 事件,但是却不会触发 mouseenter

Math.ceil 和 Math.floor 有什么区别?

Math.ceil() : 向上取整 Math.round() 四舍五入 Math.floor() : 向下取整

toPrecision 和 toFixed 和 Math.round 有什么区别?

  • toPrecision 用于处理精度,精度是从左至右第一个不为0的数开始数起。
  • toFixed 是对小数点后指定位数取整,从小数点开始数起。

直接在script标签中写 export 为什么会报错?

引入模块,须给 script 标签添加 type=“module”。如果引入脚本,则不需要 type。不加type=“module”,默认认为加载的文件是脚本而非模块,如果我们在脚本中写了 export,当然会抛错。

模块化发展历程 ???