整理-JS基础篇

107 阅读11分钟

1、作用域、作用域链

执行环境: 定义了变量或函数有权访问的其他数据。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。 执行环境分:

  • 全局执行环境:window
  • 每个函数都有自己的执行环境。它的变量对象:arguments
  • ES6之后新增了块级作用域

作用域就是变量与函数的可访问范围;
作用域([[Scope]])为所在上下文中的变量对象VO/AO。

执行环境内用到某个变量时,会在当前环境查找,找不到再往上一层执行环境查找,再找不到继续往上,直到全局执行环境,这就是作用域链。

延长作用域:(在作用域链的前端(最内层)添加一个变量对象)

  • try-catch的catch块
  • with

var是没有块级作用域的,就是说{}之内定义的变量,{}之外可以访问到。

if(true){
	var color = 'red';
}
console.log(color); // 'red'

最常见的for循环()内var定义的变量,for循环之后可以访问到。
这里还涉及到一个知识点,var声明提升。以上代码等价如下:

var color;
if(true){
	color = 'red';
}
console.log(color); // 'red'

JavaScript的执行分为:解释和执行两个阶段
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。 同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

juejin.cn/post/684490…
juejin.cn/post/684490…
juejin.cn/post/684490…

2、闭包

定义

闭包让你可以在一个内层函数中访问到其外层函数的作用域。

闭包就是能够读取其他函数内部变量的函数。

闭包有三步:
第一,外层函数嵌套内层函数;
第二, 内层函数使用外层函数的局部变量;
第三,把内层函数作为外层函数的返回值!
经过这样的三步就可以形成一个闭包! 闭包就可以在全局函数里面操作另一个作用域的局部变量!

作用

  • 封装私有变量(可以读取函数内部的变量,让变量的值始终保持在内存中)
  • 模拟模块
  • 实现块级作用域

缺点 容易造成内存泄露,使用完应尽快删掉(赋值null)

github.com/YvetteLau/S…

3、原型、原型链

每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)

原型可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得

4、继承

  1. 原型链

属性为引用类型时子类修改会影响所有的

function SuperType() { 
    this.property = true;
}
SuperType.prototype.getSuperValue = function() { 
    return this.property;
};
function SubType() { 
    this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
    return this.subproperty; 
};
let instance = new SubType(); 
console.log(instance.getSuperValue()); // true
  1. 构造函数

可以传递参数
函数不能重用

function SuperType(name){ 
    this.name = name;
}
function SubType() {
    // 继承 SuperType 并传参 
    SuperType.call(this, "Nicholas");
    // 实例属性
    this.age = 29;
}
let instance = new SubType(); 
console.log(instance.name); // "Nicholas"; 
console.log(instance.age); // 29
  1. 组合

属性用构造函数,方法用原型链
无论在什么情况下,都会调用两次父类构造函数。寄生式组合继承可避免

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() { 
    console.log(this.name);
};
function SubType(name, age){ 
    // 继承属性 
    SuperType.call(this, name);
    this.age = age; 
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { 
    console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,black" 
instance1.sayName(); // "Nicholas"; 
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); // "red,blue,green" 
instance2.sayName(); // "Greg"; 
instance2.sayAge(); // 27
  1. 原型式

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

let person = {
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob");
  1. 寄生式

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式 继承所必需的,任何返回新对象的函数都可以在这里使用。

function createAnother(original){
    let clone = object(original); // 通过调用函数创建一个新对象 
    clone.sayHi = function() { // 以某种方式增强这个对象
    	console.log("hi"); 
    };
    return clone; // 返回这个对象 
}
  1. 寄生式组合

这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。

function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); // 创建对象 
    prototype.constructor = subType; // 增强对象 
    subType.prototype = prototype; // 赋值对象
}
  1. class extends

ES5 是先创造构造函数 B 的实例,然后在让这个实例通过 A.call(this) 实现实例属性继承,在 ES6 中,是先新建父类的实例对象this,然后再用子类的构造函数修饰 this,使得父类的所有行为都可以继承。
class与function的区别:github.com/Advanced-Fr…

class Square extends Polygon {
  constructor(length) {
    super(length, length);
    this.name = 'Square';
  }
  get area() {
    return this.height * this.width;
  }
}

5、new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 链接该对象(设置该对象的constructor)到另一个对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

手写实现

function objectFactory() {
    let Constructor = [].shift.call(arguments);
    const obj = new Object();
    obj.__proto__ = Conctructor.prototype;
    Constructor.call(obj,...arguments);
    return obj;
}
function myNew(Obj,...args){
  var obj = Object.create(Obj.prototype);//使用指定的原型对象及其属性去创建一个新的对象
  Obj.apply(obj,args); // 绑定 this 到obj, 设置 obj 的属性
  return obj; // 返回实例
}

6、检查数据类型的方法

  1. typeof 不能检测null、对象、数组
typeof null // object
typeof {name: 'lily'} // object
typeof [1,2,3,4] // object
  1. instanceof 判断后者在不在前者的原型链上

  2. Object.prototype.toString.call()

function _typeof(obj){
  var s = Object.prototype.toString.call(obj);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

7、instanceOf

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function myInstanceof(target, origin) {
    // 非object直接返回false
    if(typeof target !== 'object' || target === null) return false;
    
    var proto = Object.getPrototypeOf(target);
    while (proto) {
      if (proto === origin.prototype) {
        return true
      }
      proto = Object.getPrototypeOf(proto);
    }
    return false
}

8、bind、apply、call的区别,手写实现

call

Function.prototype.myCall = function(context = window, ...args) {
  context = context || window; // 参数默认值并不会排除null,所以重新赋值
  context.fn = this; // this是调用call的函数
  const result = context.fn(...args);
  delete context.fn; // 执行后删除新增属性
  return result;
}

apply

Function.prototype.myApply = function(context = window, args = []) {
  context = context || window; // 参数默认值并不会排除null,所以重新赋值
  context.fn = this; // this是调用call的函数
  const result = context.fn(...args);
  delete context.fn;
  return result;
}

bind

Function.prototype.myBind = function(context, ...args) {
  const _this = this;
  return function Bind(...newArgs) {
    // 考虑是否此函数被继承
    if (this instanceof Bind) {
      return _this.myApply(this, [...args, ...newArgs])
    }
    return _this.myApply(context, [...args, ...newArgs])
  }
}

segmentfault.com/a/119000001…

9、js有哪些数据类型

基本类型(原始)

undefined、null、string、number、boolean、symbol

引用类型

Object、Array、Date、RegExp、Function
Map、WeakMap、Set、WeakSet
JSON

10、浅拷贝和深拷贝的区别,怎么实现

浅拷贝:引用

  1. =
  2. Object.assign (只有第一层是深拷贝)
  3. 对象解构(只有第一层是深拷贝) const newObj = {...oldObj}

深拷贝:值

  1. 通过JSON转换 var obj2 = JSON.parse(JSON.stringify(obj1));
  • 不能复制function、正则、Symbol
  • 循环引用报错
  • 相同的引用会被重复复制 TODO
  1. 递归

juejin.cn/post/684490…

11、价格精度怎么处理

小数转成整数来运算,之后再转回小数

12、遍历对象的方法有哪些?for...in 和 for...of 的区别?

  • for in
  • Object.keys()、Object.values、Object.entries()
  • Object.getOwnPropertyNames()
  • Reflect.ownKeys(obj) 区别:
  • for...in可以遍历出原型上的属性
  • for...in遍历数组的key是index,for...of遍历数组的key是value

13、this

  • 全局作用域下指向window
  • 全局函数内的this在严格模式下指向undefined、非严格模式下指向window
  • 函数内的this指向调用方(可以通过call、apply、bind改变指向)
  • 箭头函数内的this指向外层作用域,固定不变
  • 构造函数内的this指向实例
  • 事件绑定回调函数中的this指向触发事件的元素

14、setTimeout实现原理

渲染进程中所有运行在主线程上的任务都需要先添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。
要执行一段异步任务,需要先将任务添加到消息队列中。不过通过定时器设置回调函数有点特别,它们需要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,所以为了保证回调函数能在指定时间内执行,你不能将定时器的回调函数直接添加到消息队列中。
在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中
TODO

blog.poetries.top/browser-wor…

15、Event Loop

同步 -》 异步(宏 -》 微)
执行完当前一个宏任务后,执行队列中所有的微任务,再去执行下一个宏任务
我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。
等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
宏任务: setTimeout、SetInterval
微任务: MutationObserver、Promise.then
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如监听 DOM 变化。

16、数组有哪些方法

连接

  • concat
  • join 遍历
  • every
  • forEach
  • map
  • some
  • filter
  • reduce
  • reduceRight 查找
  • find
  • findIndex
  • includes
  • indexOf
  • lastIndexOf 增删
  • push
  • pop
  • shift
  • unshift
  • splice 查找
  • slice 排序
  • reverse
  • sort 其他
  • keys
  • values
  • entries
  • fill
  • flat
  • flatMap
  • copyWithin

17、 面向对象

一、封装

创建对象的方式

二、继承

18、forEach底层实现原理?怎么实现return?

Array.prototype.myEach = function(callback) {
    for (var i = 0; i < this.length; i++)
        callback(this[i], i, this);
};

通过抛出异常的方式实现return

19、怎么判断是否是数组

  • Object.prototype.toString.call(o)=='[object Array]'
  • Array.isArray()

20、隐式转换

chinese.freecodecamp.org/news/javasc…

21、创建对象的方式

  1. 字面量:const obj = {}
  2. const obj = new Object()
  3. const obj = Object.create()
  4. 构造函数: const ParentObj = (){}; const obj = new ParentObj()
  5. 工厂模式

22、自动垃圾回收机制

标记清除

1.标记阶段:把所有活动对象做上标记
3.删掉被引用对象及其引用的对象的标记
2.清除阶段:把没有标记(也就是非活动对象)销毁。

引用计数

变量被引用一次+1,被切掉引用-1,为0则回收 bug:循环引用永远不会为0

23、从输入url到页面打开发生了什么?

  1. 查找缓存,如果有就直接打开,没有继续
  2. 解析DNS,先查找缓存,没有则发起DNS请求获取IP地址
  3. 建立TCP连接
  4. 发起http请求
  5. 收到响应,需要缓存则缓存到本地
  6. 构建DOM树
  7. 构建CSS OM树
  8. 结合两者构建渲染树

24、内存泄漏

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
可能的原因:

  • 意外的全局变量
  1. 避免创建全局变量
  2. 使用严格模式
  • 闭包
  1. 不再需要的变量及时解除引用(赋值null)
  • 没有清理的dom元素的引用
  1. 及时清理
  • 定时器、事件绑定
  1. 不需要时清除、解绑

segmentfault.com/a/119000003…

预防:

  • WeakMap、WeakSet 他们对值的引用都是不计入垃圾回收机制的,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。

www.ruanyifeng.com/blog/2017/0…

参考:

blog.poetries.top/browser-wor…