js相关面试题

148 阅读33分钟

宏任务与微任务

  • 宏任务: Ajax Dom事件setTimeout/setInterval;UI rendering/UI事件;postMessage,MessageChannel. setImmediate,I/O(Node.js)

  • 微任务:promise async/await Promise2.process.nextTick(Node.js); Object.observe(已废弃;Proxy 对象替代);MutaionObserver [ www.jb51.net/article/215…]

`setTimeout(() => {`

`  ``console.log(``'定时器'``);`

` },  ``0``)`

`new` `Promise((resolve) => {`

`  ``console.log(``'同步代码'``) `

`  ``resolve(``'异步代码'``)`

`}).then((res) => {`

`  ``console.log(res);  `

`})`

`console.log(``'奥特曼'``);`

new Promise是创建一个构造函数 这个过程是同步的,而.then方法是异步的  所以代码先执行 同步>微任务>宏任务

js中的事件循环机制

JavaScript是单线程指的是同一时间只能干一件事情,只有前面的事情执行完,才能执行后面的事情。导致遇到耗时的任务时后面的代码无法执行。 同步任务 顺序执行

1.任务队列中 分为两大类 1.同步任务 2. 异步任务 2.同步代码>异步代码 

  • 执行栈 (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步,称为事件循环(Event Loop)。

 `console.log(``'1'``);`


`setTimeout(function () {`

`  ``console.log(``'2'``);`

`  ``process.nextTick(function () {`

`    ``console.log(``'3'``);`

`  ``})`

`  ``new` `Promise(function (resolve) {`

`    ``console.log(``'4'``);`

`    ``resolve();`

`  ``}).then(function () {`

`    ``console.log(``'5'``)`

`  ``})`

`})`

`process.nextTick(function () {`

`  ``console.log(``'6'``);`

`})`

`new` `Promise(function (resolve) {`

`  ``console.log(``'7'``);`

`  ``resolve();`

`}).then(function () {`

`  ``console.log(``'8'``)`

`})`

 

`setTimeout(function () {`

`  ``console.log(``'9'``);`

`  ``process.nextTick(function () {`

`    ``console.log(``'10'``);`

`  ``})`

`  ``new` `Promise(function (resolve) {`

`    ``console.log(``'11'``);`

`    ``resolve();`

`  ``}).then(function () {`

`    ``console.log(``'12'``)`

`  ``})`

`})`

第一轮 执行外面同步代码 : 1     7   

第二轮 执行 微任务 : 6    8    

第三轮 宏任务  第一个setTimeout : 同步  2  4   微任务 3   5   第二个setTimeout:同步  9   11    微任务  10   12 

整体答案:  1、7 、6、8、2、4、3、5、9、11、10、12

js的数据类型有哪些

1.基本类型 String、Number、Boolean、Null、Undefined、symbol(ES6)

2.引用类型:Object、Array、Date、Function、Error、RegExp、Math、Number、String、Boolean、Globle。

3.js内置类型有七种:String、Number、Boolean、Null、Undefined、Symbol(ES6)、Object [blog.csdn.net/qq_45471661…]

判断数据类型的方式

主要有四种常用方式 数据类型判断大概有四种typeof、instanceof、constructor、Object.prototype.toString.call()

1.typeof 基本数据类型中:Number,String,Boolean,undefined 以及引用数据类型中Function ,可以使用typeof检测数据类型,分别返回对应的数据类型小写字符。typeof 返回一个表示数据类型的字符串,返回结果包括:number、boolean、string、object、undefined、function、Symbol6种数据类型。

另:用typeof检测构造函数创建的Number,String,Boolean都返回object

基本数据类型中:null 。引用数据类型中的:Array,Object,Date,RegExp。不可以用typeof检测。都会返回小写的object

2 . instanceof 除了使用typeof来判断,还可以使用instanceof。instanceof运算符需要指定一个构造函数,或者说指定一个特定的类型,它用来判断这个构造函数的原型是否在给定对象的原型链上。(判断是否是某个类的实例)

3.constructor

constructor是prototype对象上的属性,指向构造函数。根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的

4 . 使用Object.prototype.toString.call()检测对象类型

可以通过toString() 来获取每个对象的类型。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为thisArg。

//基本数据类型是没有检测出他们的类型
var str = 'hello';
alert(str instanceof String);//false
var bool = true;
alert(bool instanceof Boolean);//false
var num = 123;
alert(num instanceof Number);//false
var nul = null;
alert(nul instanceof Object);//false
var und = undefined;
alert(und instanceof Object);//false
var oDate = new Date();
alert(oDate instanceof Date);//true
var json = {};
alert(json instanceof Object);//true
var arr = [];
alert(arr instanceof Array);//true
var reg = /a/;
alert(reg instanceof RegExp);//true
var fun = function(){};
alert(fun instanceof Function);//true
var error = new Error();
alert(error instanceof Error);//true

//使用下面的方式创建num、str、boolean,是可以检测出类型
var num = new Number(123);
var str = new String('abcdef');
var boolean = new Boolean(true);
console.log(num instanceof Number)
console.log(num instanceof String)

  • constructor:查看对象对应的构造函数

toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object xxx],xxx是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,… 基本上所有对象的类型都可以通过这个方法获取到

var str = 'hello';
console.log(Object.prototype.toString.cal(str));//[object String]
var bool = true;
console.log(Object.prototype.toString.cal(bool))//[object Boolean]
var num = 123;
console.log(Object.prototype.toString.cal(num));//[object Number]
var nul = null;
console.log(Object.prototype.toString.cal(nul));//[object Null]
var und = undefined;
console.log(Object.prototype.toString.cal(und));//[object Undefined]
var oDate = new Date();
console.log(Object.prototype.toString.cal(oDate));//[object Date]
var json = {};
console.log(Object.prototype.toString.cal(json));//[object Object]
var arr = [];
console.log(Object.prototype.toString.cal(arr));//[object Array]
var reg = /a/;
console.log(Object.prototype.toString.cal(reg));//[object RegExp]
var fun = function(){};
console.log(Object.prototype.toString.cal(fun));//[object Function]
var error = new Error();
console.log(Object.prototype.toString.cal(error));//[object Error]

apply bind call区别

1.call 方法第一个参数是要绑定给this的值,后面传入的是一个参数列表,当第一个参数为null、undefined的时候,默认指向window Fun.call(obj,'arg1', 'arg2')

2.apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组。当第一个参数为null、undefined的时候,默认指向window Fun.apply(obj,['arg1', 'arg2'])

3.bind接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个参数数组,这一点与apply相同。

区别 相同点:call() /apply() /bind() 都可以改变this指向,指向第一个参数 不同点:bind()需要手动执行

应用场景 (1)获取函数的参数数组

image.png (2)对象冒充调用

    function Box(name, age) { //构造函数模式
      this.name = name;
      this.age = age;
      this.run = function() {
          return this.name + this.age + '运行中...';
      };
  }
   var box = new Box('Lee', 100); //构造模式调用
  alert(box.run());
 var o = new Object();
  Box.call(o, 'Jack', 200) //对象冒充调用
  alert(o.run());

call、apply都是立即调用。bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数,便于稍后调用。而原函数 中的 this 并没有被改变,依旧指向原来该指向的地方。bind应用场景:给参数指定默认参数、绑定构造函数

image.png

闭包

概念

  • 闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见的方式,就是在 一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。

理解闭包,首先理解javascript特殊的变量作用域,变量的作用于无非就是两种:全局变量,局部变量。

javascript语言的特殊处就是函数内部可以读取外部作用域中的变量。

我们有时候需要得到函数内的局部变量,但是在正常情况下,这是不能读取到的,这时候就需要用到闭包。在javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包是指有权访问另一个函数作用域中的变量的函数。其本质是函数的作用域链中保存着外部函数变量对象的引用。

优缺点

  • 优点:闭包因为长期驻扎在内存中。可以重复使用变量,不会造成变量污染
  • 缺点:闭包会使函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能会导致内存泄露。解决方法是在退出函数之前,将不使用的变量全部删除。

应用场景

①函数作为参数被传递 ②函数作为返回值被返回 ③实际应用(隐藏数据):为什么说隐藏数据了呢,因为普通用户只能通过get、set等api对数据进行查看和更改等操作,没法对data直接更改,达到所谓隐藏数据的效果;jquery就利用了这一特性,必须调用$.ajax()才能访问内部属性方法。 封装功能时(需要使用私有的属性和方法), 函数防抖、函数节流

有时候需要用到函数内的局部变量,在正常情况下是不能读取到的,这个时候就需要用到闭包。

闭包可以封装对象的私有属性和方法,就是可以简单的理解成闭包就是一个私有作用域,可以定义属性和方法,vue中的data就是一种闭包的形式。

实例(封装对象的私有属性和方法)

隐藏数据 做一个简单的缓存工具

 
// 闭包隐藏数据,只提供 API
function createCache() {
    const num=100
    const data = {} // 闭包中的数据,被隐藏,不被外界访问
    return {
        num:num,
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}
 
const c = createCache()
console.log(c.num)//num此时就作为c私有属性
c.set('a', 100) //set此时作为c的私有方法
console.log( c.get('a') )

闭包作为回调函数,可以实现函数的复用 示例

<body>
    <a href="#" id="as1">20</a>
    <a href="#" id="as2">40</a>
</body>
<script>
 function changeSize(size){
     return function(){
         document.body.style.fontSize=size+'px';
     }
 }
 var size20=changeSize(20);
 var size40=changeSize(40);

 document.getElementById('as1').onclick=size20;
 document.getElementById('as2').onclick=size40;

</script>

闭包的优点

(一)变量长期驻扎在内存中
(二)另一个就是可以重复使用变量,并且不会造成变量污染
①全局变量可以重复使用,但是容易造成变量污染。不同的地方定义了相同的全局变量,这样就会产生混乱。”
②局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。
③闭包结合了全局变量和局部变量的优点。可以重复使用变量,并且不会造成变量污染

闭包的缺点

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

示例1.

//f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
function f1(){
  var n=999;
  nAdd=function(){n+=1}
  function f2(){
    alert(n);
  }
  return f2;

}
var result=f1();
result(); // 999
nAdd();
result(); // 1000

函数节流和防抖

函数节流与函数防抖的区别:我们以一个案例来讲一下它们之间的区别:设定一个间隔时间为一秒,在一分钟内,不断的移动鼠标,让它触发一个函数,打印一些内容。函数防抖:会打印1次,在鼠标停止移动的一秒后打印。函数节流:会打印60次,因为在一分钟内有60秒,每秒会触发一次。总结:节流是为了限制函数的执行次数,而防抖是为了限制函数的执行时机。

函数节流:是确保函数特定的时间内至多执行一次。

函数防抖:是函数在特定的时间内不被再调用后执行

函数防抖

指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。通俗一点:在一段固定的时间内,只能触发一次函数,在多次触发事件时,只执行最后一次。

函数防抖是指在函数被高频触发时当停止触发后延时n秒再执行函数,(即每次触发都清理延时函数再次开始计时),一般用于resize scroll,mousemove

防抖例子:搜索功能,在用户输入结束以后才开始发送搜索请求,可以使用函数防抖来实现;

/**
 * @function debounce 函数防抖
 * @param {Function} fn 需要防抖的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过防抖处理的函数
 * */
function debounce(fn, interval) {
    let timer = null; // 定时器
    return function() {
        // 清除上一次的定时器
        clearTimeout(timer);
        // 拿到当前的函数作用域
        let _this = this;
        // 拿到当前函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 开启倒计时定时器
        timer = setTimeout(function() {
            // 通过apply传递当前函数this,以及参数
            fn.apply(_this, args);
            // 默认300ms执行
        }, interval || 300)
    }
}

<body>
    <input id="input" class="ipt" type="text">
</body>
<script>
    // var ipt = document.getElementById('input')
    var ipt = document.getElementsByTagName('input')[0]
    // var ipt = document.querySelector('#input')
    // var ipt = document.querySelector('.ipt')
    //防抖
    var debounce = function(func, delay) {
        var timer = null
        return function() {
            var that = this;
            var args = arguments;

            if (timer) {
                clearTimeout(timer);
            }

            timer = setTimeout(function() {
                func.apply(that, args);
            }, delay)
        }
    }

    ipt.addEventListener('keyup', debounce(function(e) {
        console.log(e.target.value);
    }, 400))
</script>

节流

节流:在一段时间内,只能触发一次函数。 做法:触发函数时判断是否到达了指定时间,如果到达了指定时间,执行;否则不执行弹出警告 防抖与节流的常见应用:

函数节流 原理 函数被高频出发时延时n秒后才会再次执行,防抖主要是用户触发一次时间后,延迟一段时间触发, 而节流会规定的事件内触发一次事件

节流例子:在input输入框中执行输入搜索事件;滚动条scroll事件

 //就是限制一个函数在一定时间内只能执行一次。
//改变浏览器窗口尺寸,可以使用函数节流,避免函数不断执行;滚动条scroll事件,通过函数节流,避免函数不断执行。
/**
 * @function throttle 函数节流
 * @param {Function} fn 需要节流的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过节流处理的函数
 * */
function throttle(fn, interval) {
    let timer = null; // 定时器
    let firstTime = true; // 判断是否是第一次执行
    // 利用闭包
    return function() {
        // 拿到函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 拿到当前的函数作用域
        let _this = this;
        // 如果是第一次执行的话,需要立即执行该函数
        if(firstTime) {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 修改标识为null,释放内存
            firstTime = null;
        }
        // 如果当前有正在等待执行的函数则直接返回
        if(timer) return;
        // 开启一个倒计时定时器
        timer = setTimeout(function() {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 清除之前的定时器
            timer = null;
            // 默认300ms执行一次
        }, interval || 300)
    }
}

this

调用场景 1.普通函数 (this指向window)

function fn() {
       console.log(this);
   }
   fn(); //相当于下面的window.fn();
   window.fn();
  // window调用了fn,所以this指向window

2.对象方法中this(this指向调用者)

对象方法中的this,指向当前对象(因为当前对象执行了方法)。

   let pox = {
       name: '小红',
       run: function() {
           console.log(this.name) //this
       }
   }
   pox.run(); // pox 小红
   //pox调用的run,所以run方法中的this指向pox
  1. call() /apply() /bind() 改变this指向(对象方法中的this.指向方法的调用者)
  let pox={
   name:'小红',
   run:function(){
     console.log(this.name)
   }
 } 
 // 对象方法中的this.指向方法的调用者。
 pox.run();// pox 小红
 pox.run.call(obj)// 小明
 pox.run.apply(obj);// 小明
 pox.run.bind(obj)();//小明
  1. class中的this指向new 后的实例对象

class中的 this时刻指向父级的上下文对象。并且不可以被 call()/apply()/bind()修改。

  class Person{
     constructor(name,age){
      this.name=name;
      this.age=age
     }
     say(){
      console.log(`我叫${this.name}年龄是${this.age}`)
     }
   }

   let lsy=new Person('web',21);
   lsy.say();// 我叫web年龄是21
   console.log(lsy);// {name:'web',age:21}
  1. 箭头函数没有自己的this对象

普通函数指向运行时所在的对象,箭头函数指向定义时上层作用域中的,this,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

function foo() {
 setTimeout(() => {
   console.log('id:', this.id);
 }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42。`

6.setTimeout中的this

setTimeout函数中的this,相当于普通函数中的this,因为setTimeout触发的函数执行,并不是外部对象执行的。

setTimeout中函数是箭头函数,this为当前对象。因为箭头函数中的this始终是父级上下文中的this.

总结

普通函数中调用,this指向window
对象方法中调用,this指向当前对象
call apply bind中调用, this指向被传入的对象
class中的方法中调用, this指向实例对象
箭头函数,this就是父级上下文中的this

原型与原型链

ECMAScript5篇查看 : juejin.cn/post/712313…

prototype 每一个函数都会有prototype属性,被称为显式原型

proto 每一个实例对象都会有__proto__属性,其被称为隐式原型

每个函数function都有一个prototype,即原型。每个对象都有一个__proto__,为隐式原型。

如上图,每个对象都有一个__proto__属性,指向创建该对象的函数的prototype

获取对象时,如果这个对象身上本身没有这个属性时,它就会去他的原型__proto__上去找,如果还找不到,就去原型的原型上去找…一直找到最顶层(Object.prototype)为止,Object.prototype对象也有__proto__属性值为null。

image.png

instanceof的判断规则往下面的例子看

function Foo(){};
var f1=new Foo();
alert(f1 instanceof Foo) //会返回true

image.png 然后instanceof的判断规则就是:instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是函数,称为B。沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,就是返回true,如果找到终点都没有重合就返回false。

基本类型和引用类型的区别

1.基本类型和引用类型的值

ECMAScript 变量可能包含两种不同的数据类型的值:基本类型值和引用类型值。基本 类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。 而引用类型值则是指那些保存在堆内存中的对象,意思是变量中保存的实际上只是一个指 针,这个指针指向内存中的另一个位置,该位置保存对象。 将一个值赋给变量时,解析器必须确定这个值是基本类型值,还是引用类型值。基本类 型值有以下几种:Undefined、Null、Boolean、Number 和 String。这些类型在内存中分别占 有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的。 PS:在某些语言中,字符串以对象的形式来表示,因此被认为是引用类型。ECMAScript 放弃这一传统。 如果赋值的是引用类型的值,则必须在堆内存中为这个值分配空间。由于这种值的大小 不固定,因此不能把它们保存到栈内存中。但内存地址大小的固定的,因此可以将内存地址 保存在栈内存中。这样,当查询引用类型的变量时,先从栈中读取内存地址,然后再通过地 址找到堆中的值。对于这种,我们把它叫做按引用访问。 image.png

区别 1.引用类型有动态属性,基本类型会报错

   box = new Object(); //创建引用类型
   box.name = 'Lee'; //新增一个属性
   alert(box.name); //输出
   var box = 'Lee'; //创建一个基本类型
   box.age = 27; //给基本类型添加属性
   alert(box.age); //undefined

2.变量复制时,基本类型复制的是值本身,而引用 类型复制的是地址。

   var box = 'Lee'; //在栈内存生成一个 box 'Lee'
   var box2 = box; //在栈内存再生成一个 box2 'Lee

image.png 在引用类型中,box2 其实就是 box,因为他们指向的是同一个对象。如果这个对象中的 name 属性被修改了,box2.name 和 box.name 输出的值都会被相应修改掉了。

   var box = new Object(); //创建一个引用类型
   box.name = 'Lee'; //新增一个属性
   var box2 = box; //把引用地址赋值给 box2

3.:当使用 instanceof 检查基本类型的值时,它会返回 false

var num = 123; alert(num instanceof Number);//false
var box = [1, 2, 3];alert(box instanceof Array); //是否是数组true
 

4.instanceof可以判断一个变量是否为数组

  let obj = {'name': 'kankk'};
 let arr = [1, 2];
 console.log(obj instanceof Object); // true
 console.log(obj instanceof Array); // false
 console.log(arr instanceof Object); // true
 console.log(arr instanceof Array); // true

image.png

执行环境及作用域及作用域链

执行环境

执行环境是JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的 其他数据,决定了它们各自的行为。 全局执行环境是最外围的执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象。因此所有的全局变量和函数都是作为 window 对象的属性和方法创建的。

作用域

作用域就是一个变量可以使用的范围,主要分为全局作用域和函数作用域和块级作用域

全局作用域就是Js中最外层的作用域

函数作用域是js通过函数创建的一个独立作用域,函数可以嵌套,所以作用域也可以嵌套

Es6中新增了块级作用域(由大括号包裹,比如:if(){},for(){}等)

作用域链

当前作用域外的变量都是自由变量,一个变量在当前作用域没有定义,但是被使用了,就会向上级作用域,一层一层依次查找,直至找到为止,找到这个变量后就会停止,不会继续查找这个变量,如果全局作用域都没有找到这个变量就会报错。这个自由变量查找的过程就是作用域链。

 var box = 'blue'; //声明一个全局变量
    function setBox() {
        alert(box); //全局变量可以在函数里访问
    }
    setBox(); //执行函数

PS:当执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和 函数定义也随之销毁。如果是全局环境下,需要程序执行完毕,或者网页被关闭才会销毁。 PS:每个执行环境都有一个与之关联的变量对象,就好比全局的 window 可以调用变量 和属性一样。局部的环境也有一个类似 window 的变量对象,环境中定义的所有变量和函数 都保存在这个对象中。(我们无法访问这个变量对象,但解析器会处理数据时后台使用它)

函数里的局部作用域里的变量替换全局变量,但作用域仅限在函数体内这个局部环境。

    var box = 'blue';

    function setBox() {
        var box = 'red'; //这里是局部变量,出来就不认识了
        alert(box);
    }
    setBox();
    alert(box);

通过传参,可以替换函数体内的局部变量,但作用域仅限在函数体内这个局部环境

    var box = 'blue';

    function setBox(box) { //通过传参,替换了全局变量
        alert(box);
    }
    setBox('red');
    alert(box);

函数体内还包含着函数,只有这个函数才可以访问内一层的函

 var box = 'blue';
 function setBox() {
     function setColor() {
         var b = 'orange';
         alert(box);
         alert(b);
     }
     setColor(); //setColor()的执行环境在 setBox()内
 }
 setBox();

每个函数被调用时都会创建自己的执行环境。当执行到这个函数时,函数的环境 就会被推到环境栈中去执行,而执行后又在环境栈中弹出(退出),把控制权交给上一级的执 行环境。

当代码在一个环境中执行时,就会形成一种叫做作用域链的东西。它的用途是保 证对执行环境中有访问权限的变量和函数进行有序访问。作用域链的前端,就是执行环境的 变量对象。 变量查询中,访问局部变量要比全局变量更快,因为不需要向上搜索作用域链。 image.png

没有块级作用域 块级作用域表示诸如 if 语句等有花括号封闭的代码块,所以,支持条件判断来定义变 量。

 if (true) { //if 语句代码块没有局部作用域
       var box = 'Lee';
   }
   alert(box)

for 循环语句也是如此

  for (var i = 0; i < 10; i++) { //没有局部作用域
        var box = 'Lee';
    }
    alert(i);//10
    alert(box);//Lee

let var const 区别

变量提升

每个var声明的变量,function声明的函数存在变量提升。let const不存在变量提升 var声明的变量,function声明的函数存在变量提升
let const 不会变量提升

在js中声明之前未定义,会在js的最上方会形成一个预解析池,用来存储声明了但没有先定义的变量名

let

1.var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined

// var 的情况
  console.log(foo); // 输出undefined
  var foo = 2;
  // let 的情况
 console.log(bar); // 报错ReferenceError
 let bar = 2;

2.只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。隐蔽的死区问题

   var tmp = 123;

   if (true) {
     tmp = 'abc'; // ReferenceError
     let tmp;
   }

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

3.不允许重复声明(let不允许在相同作用域内,重复声明同一个变量)

// 报错
function func() {
 let a = 10;
 var a = 1;
}

// 报错
function func() {
 let a = 10;
 let a = 1;
}

4.为什么需要块级作用域 1.内层变量会覆盖外层变量

`if`代码块的外部使用外层的`tmp`变量,内部使用内层的`tmp`变量。但是,函数`f`执行后,输出结果为`undefined`,原因在于变量提升,导致内层的`tmp`变量覆盖了外层的`tmp`变量。
var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined
  1. 用来计数的循环变量泄露为全局变量。
变量`i`只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
 console.log(s[i]);
}
console.log(i); // 5

let块级作用域

函数有两个代码块,都声明了变量`n`,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用`var`定义变量`n`,最后输出的值才是 10function f1() {
let n = 5;
if (true) {
 let n = 10;
}
console.log(n); // 5
}

var

1 var声明作用域

var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问

function test() {
   var message = "hello world";   // 局部变量
}
test();
console.log(message);  // 报错
//函数test()调用时会创建变量message并给它赋值,调用之后变量随即被销毁。因此,在函数test()之外调用变量message会报错

//在函数内定义变量时省略var操作符,可以创建一个全局变量
function test() {
   message = "hello world";   // 局部变量
}
test();
console.log(message);  // hello world
//省略掉var操作符之后,message就变成了全局变量。只要调用一次函数test(),就会定义这个变量,并且可以在函数外部访问到。在局部作用域中定义的全局变量很难维护,不推荐这么做。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出ReferenceError。

2.var 声明提升 var在js中是支持预解析的,如下代码不会报错。这是因为使用var声明的变量会自动提升到函数作用域顶部:

function foo() {
    console.log(age);
    var age = 26;
}
foo(); // undefined
//javaScript引擎,在代码预编译时,javaScript引擎会自动将所有代码里面的var关键字声明的语句都会提升到当前作用域的顶端,如下代码:
function foo() {
    var age;
    console.log(age);
    age = 26;
}
foo(); // undefined
会把声明提前,以下会先打印出undefined,再打印出10**
console.log(a)
var a = 10
console.log(a)
相当于
var a
console.log(a);//undefined
a = 10
console.log(a) //10

函数声明也是

**相当于把整个fn提到作用域的最上面,所以调用fn时会正常打印jack
fn('jack');//jack
function fn (name){
console.log(name)
}

函数表达式不行

**函数表达式,JavaScript会把var fn提到作用域最上面,没有吧函数提上去,所以会报错。**
fn("jack");//报错
var fn = function(name) {
console.log(name);
};
  1. 全局声明
var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined

变量提升

概念的字面意义上说,变量提升 意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中时变动。

  • 函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
 // Example 1 - 变量 y 被提升了,但是它的初始化没有
var x = 1;                 // 声明 + 初始化 x
console.log(x + " " + y);  // 输出:'1 undefined'
var y = 2;                 // 声明 + 初始化 y

// Example 2 - 先赋值后声明也能连名带值被提升
var num1 = 3;                   // Declare and initialize num1
num2 = 4;                       // Initialize num2
console.log(num1 + " " + num2); // 输出:'3 4'
var num2;                       // Declare num2 for hoisting

// Example 3 - 同理
a = 'Cran';              // Initialize a
b = 'berry';             // Initialize b
console.log(a + " " + b); // 输出:'Cranberry'
var a, b;                // Declare both a & b for hoisting

js的垃圾回收机制

一、 垃圾回收方式通常有两种方式

  • 标记清除(mark and sweep) 这是JavaScript中最常用的垃圾回收方式。

(1)当变量进入执行环境时(函数中声明变量),就标记这个变量为“进入环境”,当变量离开环境时(函数执行结束),则将其标记为“离开环境”,离开环境之后还有的变量则是需要被删除的变量。

(2)垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记。

(3)去掉环境中的变量以及被环境中变量引用的变量的标记。

(4)之后再被加上标记的变量即是需要回收的变量(因为环境中的变量已经无法访问到这些变量)

(5)最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

  • 引用计数(reference counting) 这种方式常常会引起内存泄漏,低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时,则说明没有办法再访问这个值了,被视为准备回收的对象,每当过一段时间开始垃圾回收的时候,就把被引用数为0的变量回收。引用计数方法可能导致循环引用,类似死锁,导致内存泄露。

示例

//objA和objB相互引用,两个对象的引用次数都是2。函数执行完成之后,objA和objB还将会继续存在,因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露。
function problem() {
    var objA = new Object();
    var objB = new Object();

    objA.someOtherObject = objB;
    objB.anotherObject = objA;
}

二、 常见内存泄漏的原因

(1)全局变量引起的内存泄露

(2)闭包引起的内存泄露:慎用闭包

(3)dom清空或删除时,事件未清除导致的内存泄漏

(4)循环引用带来的内存泄露

深拷贝和浅拷贝

  • 浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用。
  • 浅拷贝是拷贝的内存地址,使新对象指向拷贝对象的内存地址。深拷贝是重新开辟一块内存空间,用来存放sources对象的值。
  • 浅拷贝后改变target中的值,sources也会进行改变。深拷贝不会这样

一、浅拷贝的方式 它们的成员都是对原数组成员的引用,这就是浅拷贝 (1)concat

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a3 = a1.concat(a2);

(2)ES6的解构

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a4 = [...a1, ...a2];

(3)slice

    let arr = [{
       name:'xkx',
       age:18
   }]
   let newarr = arr.slice(0,1)

(4) object.assign

  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);

(5) for in 遍历循环

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

  }
  let obj = {
      name: 'xxx',
      age: 18
  }
  console.log(simpleClone(obj))

二、深拷贝的方式 1.JSON.parse,JSON.stringify

这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。

弊端:它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。

function deepClone(initalObj) {
   var obj = {};
   try {
       obj = JSON.parse(JSON.stringify(initalObj));
   }
   return obj;
}

2.递归拷贝 www.jb51.net/article/990…

  function deepClone(initalObj, finalObj) {
      var obj = finalObj || {};
      for (var i in initalObj) {
          if (typeof initalObj[i] === 'object') {
              obj[i] = (initalObj[i].constructor === Array) ? [] : {};
              arguments.callee(initalObj[i], obj[i]);
          } else {
              obj[i] = initalObj[i];
          }
      }
      return obj;
  }

问题:但是当遇到两个互相引用的对象,会出现死循环的情况 为了避免相互引用的对象导致死循环的情况,则应该在遍历的时候判断是否相互引用对象,如果是则退出循环。

改进版

 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) ? [] : {};
          arguments.callee(prop, obj[i]);
      } else {
          obj[i] = prop;
      }
  }
  return obj;
}

写法2

 function getType(obj) {
      //tostring会返回对应不同的标签的构造函数
      var toString = Object.prototype.toString;
      var map = {
          '[object Boolean]': 'boolean',
          '[object Number]': 'number',
          '[object String]': 'string',
          '[object Function]': 'function',
          '[object Array]': 'array',
          '[object Date]': 'date',
          '[object RegExp]': 'regExp',
          '[object Undefined]': 'undefined',
          '[object Null]': 'null',
          '[object Object]': 'object'
      };
      if (obj instanceof Element) {
          return 'element';
      }

      return map[toString.call(obj)];
  }
  
  function deepClone(data){
     var type = getType(data);
     var obj;
     if(type === 'array'){
         obj = [];
     } else if(type === 'object'){
         obj = {};
     } else {
         //不再具有下一层次
         return data;
     }
     if(type === 'array'){
         for(var i = 0, len = data.length; i < len; i++){
             obj.push(deepClone(data[i]));
         }
     } else if(type === 'object'){
         for(var key in data){
             obj[key] = deepClone(data[key]);
         }
     }
     return obj;
 }

通过object.create改进

    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;
}

写法2 对于function类型,这里是直接赋值的,还是共享一个内存值。这是因为函数更多的是完成某些功能,有个输入值和返回值,而且对于上层业务而言更多的是完成业务功能,并不需要真正将函数深拷贝。

 function getType(obj) {
      //tostring会返回对应不同的标签的构造函数
      var toString = Object.prototype.toString;
      var map = {
          '[object Boolean]': 'boolean',
          '[object Number]': 'number',
          '[object String]': 'string',
          '[object Function]': 'function',
          '[object Array]': 'array',
          '[object Date]': 'date',
          '[object RegExp]': 'regExp',
          '[object Undefined]': 'undefined',
          '[object Null]': 'null',
          '[object Object]': 'object'
      };
      if (obj instanceof Element) {
          return 'element';
      }

      return map[toString.call(obj)];
  }
  
  function deepClone(data){
     var type = getType(data);
     var obj;
     if(type === 'array'){
         obj = [];
     } else if(type === 'object'){
         obj = {};
     } else {
         //不再具有下一层次
         return data;
     }
     if(type === 'array'){
         for(var i = 0, len = data.length; i < len; i++){
             obj.push(deepClone(data[i]));
         }
     } else if(type === 'object'){
         for(var key in data){
             obj[key] = deepClone(data[key]);
         }
     }
     return obj;
 }

写法3 使用树的广度优先遍历来实现

 //这里为了阅读方便,只深拷贝对象,关于数组的判断参照上面的例子
 function deepClone(data){
     var obj = {};
     var originQueue = [data];
     var copyQueue = [obj];
     //以下两个队列用来保存复制过程中访问过的对象,以此来避免对象环的问题(对象的某个属性值是对象本身)
     var visitQueue = [];
     var copyVisitQueue = [];
     while(originQueue.length > 0){
         var _data = originQueue.shift();
         var _obj = copyQueue.shift();
         visitQueue.push(_data);
         copyVisitQueue.push(_obj);
         for(var key in _data){
             var _value = _data[key]
             if(typeof _value !== 'object'){
                 _obj[key] = _value;
             } else {
                 //使用indexOf可以发现数组中是否存在相同的对象(实现indexOf的难点就在于对象比较)
                 var index = visitQueue.indexOf(_value);
                 if(index >= 0){
                     // 出现环的情况不需要再取出遍历
                     _obj[key] = copyVisitQueue[index];
                 } else {
                     originQueue.push(_value);
                     _obj[key] = {};
                     copyQueue.push(_obj[key]);
                 }
             }
         }
     }
     return obj;
 }

3.loadsh 的deepClone方法

4.Object.create

/ 写法一
const clone1 = {
 __proto__: Object.getPrototypeOf(obj),
 ...obj
};

// 写法二
const clone2 = Object.assign(
 Object.create(Object.getPrototypeOf(obj)),
 obj
);

// 写法三
const clone3 = Object.create(
 Object.getPrototypeOf(obj),
 Object.getOwnPropertyDescriptors(obj)
)

for in 和for of的区别

blog.csdn.net/zhaofeiweb/… blog.csdn.net/weixin_4829…

for of

优点:

  • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  • 不同于forEach方法,它可以与breakcontinuereturn配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

ES6提出的语句,在可迭代对象(Array,Map,Set,String,TypedArray,arguments)上创建一个迭代循环。

for…of只能应用于可迭代对象,即拥有[Symbol.iterator] 属性的collection对象,并不适用于所有的object.

对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。 示例:

  let obj = {
     name: 'aa',
     age: 12,
     sex: 'man'
 };
 for (let item of obj) {
     console.log(item);
 }

image.png

         let es6 = {
            edition: 6,
            committee: "TC39",
            standard: "ECMA-262"
        };

        for (let e in es6) {
            console.log(e);
        }
        // edition
        // committee
        // standard

        for (let e of es6) {
            console.log(e);
        }
     // TypeError: es6[Symbol.iterator] is not a function

解决方式: (1)一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组

  for (var key of Object.keys(someObject)) {
    console.log(key + ': ' + someObject[key]);
  }

(2)使用 Generator 函数将对象重新包装一下。

const obj = { a: 1, b: 2, c: 3 }

function* entries(obj) {
 for (let key of Object.keys(obj)) {
   yield [key, obj[key]];
 }
}

for (let [key, value] of entries(obj)) {
 console.log(key, '->', value);
}

(3)空对象obj部署了数组arrSymbol.iterator属性,结果objfor...of循环,产生了与arr完全一样的结果。

    const arr = ['red', 'green', 'blue'];

    for(let v of arr) {
      console.log(v); // red green blue
    }

    const obj = {};
    obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

    for(let v of obj) {
      console.log(v); // red green blue
    }

(4)不同于forEach方法,它可以与breakcontinuereturn配合使用。

    for (var n of fibonacci) {
    if (n > 1000)
    break;
    console.log(n);
  }

(5) for...of循环可以代替数组实例的forEach方法。

const arr = ['red', 'green', 'blue'];

arr.forEach(function (element, index) {
  console.log(element); // red green blue
  console.log(index);   // 0 1 2
});

(6) JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
 console.log(a); // 0 1 2 3
}

for (let a of arr) {
 console.log(a); // a b c d
}

for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法

for in

for in 既可以遍历数组又可以遍历对象 缺点:

  • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in循环会以任意顺序遍历键名。
let arr = [{
name: 'aa',
age: 12
}, {
name: 'bb',
age: 13
}, {
name: 'cc',
age: 14
}];
for (let item in arr) {
console.log(item);
}

image.png 对象遍历属性

    let obj = {
        name: 'aa',
        age: 12,
        sex: 'man'
    };
    for (let item in obj) {
        console.log(item);
    }

image.png

区别

1.for…in 遍历(当前对象及其原型上的)每一个属性名称,for…of遍历(当前对象上的)每一个属性值,如果获取数组的索引,可以借助数组实例的entries方法和keys方法

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

2.for…in循环会遍历一个object所有的可枚举属性。

(1)对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。 有四个操作会忽略enumerablefalse的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

这四个操作之中,前三个是 ES5 就有的,最后一个Object.assign()是 ES6 新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

3.for…of只能应用于可迭代对象,即拥有[Symbol.iterator] 属性的collection对象,并不适用于所有的object.

4.for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

`for...of`循环不会返回数组`arr``foo`属性。
let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

ES6 一共有 5 种方法可以遍历对象的属性及区别

developer.mozilla.org/zh-CN/docs/…

(1)for...in

for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

   var triangle = {a:1, b:2, c:3};

function ColoredTriangle() {
 this.color = "red";
}

ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();

for (var prop in obj) {
   console.log("o." + prop + " = " + obj[prop]);
}

//原型上的可枚举数据也被迭代出来
//o.color = red
//o.a = 1
//o.b = 2
//o.c = 3

//使用hasOwnProperty()

for (var prop in obj) {
 if( obj.hasOwnProperty( prop ) ) {
   console.log("o." + prop + " = " + obj[prop]);
 } 
}
//o.color = red

(2)Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 验证方式

  var obj = {};
    obj.name = 'xkx';
    obj.age = 18;
    obj.run = function() { //创建一个 run()方法并返回值
        return this.name + this.age + '运行中...';
    };
    // 给原型添加属性和方法
    Object.prototype.gaga = function() {
        console.log('gaga')
    }
    Object.prototype.names = 'names'
        // 给原型添加一个可枚举的属性
    Object.defineProperty(Object.prototype, "ages", {
        enumerable: false,
        configurable: false,
        writable: false,
        value: 20
    });
    // var descriptor = Object.create(null); // 没有继承的属性
    // descriptor.value = 'static';
    // // 默认没有 enumerable,没有 configurable,没有 writable
    // Object.defineProperty(obj, 'key', descriptor);
    // console.log(Object.getOwnPropertyDescriptor(obj, 'key'))

    //显式
    Object.defineProperty(obj, "key", {
        enumerable: true,
        configurable: false,
        writable: false,
        value: "static"
    });

    for (var i in obj) {
        console.log(i)
    }
    console.log(Object.keys(obj))

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

(5)Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

谈一下迭代器

概念 JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 作用 一是为各种数据结构,提供一个统一的、简便的访问接口;

二是使得数据结构的成员能够按某种次序排列;

三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

makeIterator函数,它是一个遍历器生成函数

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

类似数组的对象调用数组的Symbol.iterator方法的例子

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

普通对象部署数组的Symbol.iterator方法,并无效果。

let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

调用 Iterator 接口的场合

(1)解构赋值

(2)扩展运算符

(3)yield

(4)其他场合 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for...of

  • Array.from()

  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])

  • Promise.all()

  • Promise.race()

js中常见的设计模式

跨域及处理方法

es6中的proxy 及reflect

reflect

概述 (1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。

(2)修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false应用 13个静态方法 每个静态方法都有相应的应用场景 具体参官网:es6.ruanyifeng.com/#docs/refle…

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

示范 (1)判断类型

var x = {
       toString() {
           return "X";
       },
   };

   x.toString(); // => "X"

   Object.prototype.toString.call(x); // => "[object Object]"

   Reflect.apply(Object.prototype.toString, x, []); // => "[object Object]"

Object.prototype.toString

1.对象或数组都具有 toLocaleString()、toString()和 valueOf()方法。其中 toString()和 valueOf() 无论重写了谁,都会返回相同的值。数组会讲每个值进行字符串形式的拼接,以逗号隔开

2.每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。

3.Number、Boolean、String、Array、Date、RegExp、Function这几种构造函数生成的对象,通过toString转换后会变成相应的字符串的形式,因为这些构造函数上封装了自己的toString方法

Number.prototype.hasOwnProperty('toString'); // true
Boolean.prototype.hasOwnProperty('toString'); // true
String.prototype.hasOwnProperty('toString'); // true
Array.prototype.hasOwnProperty('toString'); // true
Date.prototype.hasOwnProperty('toString'); // true
RegExp.prototype.hasOwnProperty('toString'); // true
Function.prototype.hasOwnProperty('toString'); // true
var num = new Number('123sd');
num.toString(); // 'NaN'
var str = new String('12df');
str.toString(); // '12df'
var bool = new Boolean('fd');
bool.toString(); // 'true'
var arr = new Array(1,2);
arr.toString(); // '1,2'
var d = new Date();
d.toString(); // "Sun Jul 22 2018 12:38:42 GMT+0800 (中国标准时间)"
var func = function () {}
func.toString(); // "function () {}"

其他

    var obj = new Object({});
    obj.toString(); // "[object Object]"
    Math.toString(); // "[object Math]

大多数对象,toString() 方法都是重写了的,这时,需要用 call() 或 Reflect.apply() 等方法来调用

var x = {
        toString() {
            return "X";
        },
    };

    x.toString(); // => "X"

    Object.prototype.toString.call(x); // => "[object Object]"

    Reflect.apply(Object.prototype.toString, x, []); // => "[object Object]"

[blog.csdn.net/u011140116/…] [zhuanlan.zhihu.com/p/118793721]