JS基础知识深刻总结

本系列将从如下专题去总结:javascript

1. JS基础知识深刻总结
2. 对象高级
3. 函数高级
4. 事件对象与事件机制前端

暂时会对以上四个专题去总结,如今开始JS之旅的第一部分:JS基础知识深刻总结。下图是我这篇的大纲。 java

js基础大纲前端学习大纲

话在前面:我一直都认为,在互联网学习的大环境下,网路学习资源不少,但这些博客等仅仅是一个向导,一个跳板,真正学习知识仍是须要线下,须要书籍。 另外,若有错误,请留言或私信。一块儿成长,谢谢。git

1.1 数据类型的分类和判断

1.1.1 数据类型的分类

  • 基本(值)类型 [primitive values]
基本类型 类型的值 检测方法
Number 能够任意数值 用typeof检测结果为number
String 能够任意字符串 用typeof检测结果为string
Boolean 只有true/false 用typeof检测结果为boolean
undefined 只有undefined 用typeof检测数据类型和‘===’(全等符号)
null 只有null ‘===’(全等符号)
Symbol 经过Symbol()获得,值可任意 用typeof可检测结果为symbol
  • 对象(引用)类型 [reference values]
对象类型 描述 检测方法
Object 能够任意对象 能够用typeof/instanceof检测数据类型
Array 一种特别的对象(有数值下标,并且内部数据是有序的。通常的对象内部的数据是无序的,好比你一个对象中有name和age,他们是无序的。) instanceof
Function 一种特别的对象(能够去执行的对象,内部包含可运行的代码。一个普通的对象能够执行吗?不能。)另外,对象是存储一些数据的,固然函数也是存储一些代码数据。 typeof
Date 时间对象 instanceof
RegExp 正则对象 instanceof

1.1.2 基本/对象数据类型特色比较

基本数据类型 对象类型
基本类型的值是不可变的 引用类型的值是可变的
基本类型的比较是它们的值的比较 引用类型的比较是引用(指针指向)的比较
基本类型的变量是存放在栈内存(Stack)里的 引用类型的值是保存在堆内存(Heap)中的对象(Object)

1.1.3 数据类型的判断

  • typeof 注1:用typeof判断返回数据类型的字符串(小写)表达。好比:typeof ‘hello’ 结果是 string。 注2:用typeof来测试有如下七种输出结果:number string boolean object function symol undefined。 所以typeof不能去判断出nullobject,由于用typeof去判断null会输出object。 注3:全部的任何对象,用typeof测试数据类型都是object。所以,typeof不能去判断出objectarraygithub

  • ===(全等符号) 注1:只能够判断undefined 和 null 由于这两种基本类型的值是惟一的,便可用全等符比较。面试

  • instanceof 注1:A instanceof B 翻译就是B的实例对象是A 吗? 判断对象的具体类型(究竟是对象类型中的Object Array Function Date RegExp的哪个具体的类型),返回一个Boolean值。ajax

  • 借调法:Object.prototype.toString.call() 注1:这种方法只能够检测出内置类型(引擎定义好的,自定义的不行),这种方法是相对而言更加安全。Object Date String Number RegExp Boolean Array Math Window等这些内置类型。算法

以上说明都有案例在面试题里json

1.1.3 四个常见问题

  • 问题1:undefined与报错(not defined)的区别? 对象.属性:属性不存在则返回undefined 访问变量:变量不存在则报错,xx is not defined
    var obj={ name:'lvya' };
    console.log(obj.age); //undefined
    console.log(age);  //报错,age is not defined
    复制代码
    从这个点再去看一个简单的例子:
    function Person(name,age,price) {
      this.name = name
      this.age = age
      this.price=price
      setName=function (name) {
        this.name=name;
      }
    }
    
    var p1 = new Person('LV',18,'10w')
    console.log(p1.price); // 10w
    复制代码
    根据上面这个例子,问个问题。请问访问p1.price先找啥?后找啥?经过啥来找?(问题问的很差,直接看答案吧) An:p1.price先找p1后找 price 。 p1是一个全局变量哦,这个全局变量自己存在栈内存中,它的值是一个地址值,指向new Person 出来的对象。怎么找呢?先找p1是沿着做用域找的,后找price是沿着原型链找的。这就是联系,从另一个方面细看问题。可能这样看问题,你就能够把原型链和做用域能够联系起来思考其余问题。

串联知识点:请你讲讲什么是原型链和做用域链? 咱们从a.b这个简单的表达式就能够看出原型链和做用域链。(a正如上例的p1)第一步先找a!a是一个变量,经过做用域链去查找,一层一层往外找,一直找到最外层的window,还没找到那就会报错,a is not defined。 找到a这个变量,它的值有两种状况:基本数据类型和对象类型。 若是是基本数据类型(除了undefined和null)会使用包装类,生成属性。若是是undefined和null就会报错,显示不能读一个undefined或null的属性。 若是是对象类型,这就是对象.属性的方式,开始在对象自身查找,找不到沿着原型链去找。原型链也找不到的时候,那么就会输出undefined数组

原型与原型链图解

  • 问题2:undefined与null的区别? undefined表明定义未赋值 nulll定义并赋值了, 只是值为null

    var a;
        console.log(a);  // undefined
        a = null;
        console.log(a); // null
    复制代码

    使用Object.prototype.toString.call()形式能够具体打印类型来区别undefined和null。 若是值是undefined,返回“[object Undefined]”。 若是这个值为null,则返回“[object Null]”。

  • 问题3:何时给变量赋值为null 呢? 初始赋值, 代表这个变量我要去赋值为对象 结束前, 这个对象再也不使用时,让对象成为垃圾对象(被垃圾回收器回收)

    //起始
        var b = null   // 初始赋值为null, 代表变量b将要赋值为对象类型
        //肯定赋值为对象
        b = ['lvya', 12]
        //结束,当这个变量用不到时
        b = null  // 让b指向的对象成为垃圾对象(被垃圾回收器回收)
        // b = 2 //固然让b=2也能够,但不常使用
    复制代码
  • 问题4:变量类型与数据类型同样吗? 数据的类型:包含基本数据类型和对象类型 变量的类型(实则是变量内存值的类型) JS弱类型语言,变量自己是无类型的。包含基本类型: 保存的就是基本类型的数据(好比:数字1,字符串‘hello lvya’,布尔值false等)和引用类型: 保存的是地址值,这个地址值去指向某个对象。

1.1.4 一张图看懂JavaScript各种型的关系

JavaScript各种型的关系

1.1.5 谈谈valueOf( ) 与 toString( )

toString()valueOf() 都是在Object.prototype里面定义.

  1. toString() 表示的含义是把这个对象表示成字符串形式, 而且返回这个字符串形式. 首先,在Object.prototype中它对toString()方法的默认实现是"[object Object]"。 验证一下:

    var p={};   
       console.log(p.toString());   //[object Object] 去Object.prototype的去找(输出他的默认实现)
       function Person(){
       }
       var p1=new Person();
       console.log(p1.toString());  //[object Object] 去Object.prototype的去找(输出他的默认实现)
    复制代码

    再看一下能够在本身的对象或者原型上对 toString() 进行覆写(重写, override)。这时访问这个对象的toString()方法时,就会沿着原型链上查找,恰好在自身对象上就找到了toString(),这个时候就再也不去找原型链上的顶端Object.prototype的默认的toString()啦,便实现了对象的toString()的重写。 验证一下:

    var p = {
       toString: function (){
         return "100";
         }
       };
       //100 这个时候就会在首先在P对象上找toString()方法,这个时候就是对toString方法的重写
       console.log(p.toString());  
    复制代码

    再举一个重写的栗子:

    var date = new Date();
      console.log(date.toString());   
      //Fri Jan 18 2019 21:13:44 GMT+0800 (中国标准时间)
      /*从输出结果可知,Date这个构造函数的原型实际上是有toString()方法的, 说明JS引擎已经在Date原型对象中重写了toString()方法, 故不会在Object.prototype中找*/
      console.log(Date.prototype);  //发现确实有toString()方法
    
      var n = new Number(1);
      console.log(n.toString());  //1(字符串)
      /* 同理:这就是说明他们在js引擎内置的包装对象,说白了,就是内部已经给Number对象上重写了 toString()方法。这个方法恰好就是将数字转为字符串*/
    复制代码
  2. valueOf() 应该返回这个对象表示的基本类型的值!在Object.prototype.valueOf 中找到, 默认返回的是this。当须要在对象上重写valueOf()时,应该是返回一个基本数据类型的值。 先看一个默认返回的值的状况。(也就是说它是去这个对象的原型链的顶端Object.prototype.valueOf 找的valueOf方法 )

    function Person(){
      }
     var p1 = new Person();
     console.log(p1.valueOf() == p1);  //true 
    复制代码

    对返回结果的说明:这个时候p1.valueOf是在Object.prototype.valueOf找到的,返回值默认this。此时this就是p1的这个对象。故结果返回true。 如今看一下重写valueOf后的状况

    var p = {
          toString: function (){
            return "100";
          },
          valueOf : function (){
            return 1;
          }
        };
        console.log(p.toString());  //100(字符串) 
        //还来不及去Object.prototype.valueOf 其自己就有了toString方法 故固然读自己对象的toString()方法
        console.log(p.valueOf());  //1(number数据类型) 
        //同理,没去Object.prototype.valueOf找 而是找其自己的valueOf方法
    复制代码

    咱们再来验证JS引擎对那些内置对象有去重写toString()valueOf()呢?

    var n = new Number(100);
    console.log(n.valueOf());  //100 (number类型)
    
    var s = new String("abc");
    console.log(s.valueOf());  //abc (string类型)
    
    var regExp = /abc/gi;
    console.log(regExp.valueOf() === regExp);  //true 
    //说明这个时候正则对象上没有valueOf,是在Object.prototype.valueOf找的,返回this,this指的就是regExp正则对象。
    复制代码

    结论:在JS中, 只有基本类型中那几个包装类型进行了重写, 返回的是具体的基本类型的值, 其余的类型都没有重写,是去对象原型链的顶层Object.prototype.valueOf去找的。

1.1.6 数据类型间的比较

了解完valueOf()toSting()方法后,其实他们就是对象与基本数据类型的比较的基础。咱们数据类型,分为基本数据类型和对象类型两种,故在数据类型比较中,只会有三种状况:

  1. 基本数据类型间的比较
  2. 对象类型间的比较
  3. 基本数据类型与对象类型间的比较

基本数据类型间的比较

规则:若是类型相同,则直接比较; 若是类型不一样, 都去转成number类型再去比较 三个特殊点:1. undefined == null 2. 0undefined, 0null都不等 3. 若是有两个 NaN 参与比较,则老是不等的。

总结:都是基本数据类型,但当类型不一样时,转为number类型的规律以下:

基本类型中非number类型 转为number类型
undefined ‘12a’ ‘abc’ ‘\’ Nan
'' ' ' '\t' '0' null false 0
true ‘1’ 1
‘12’ 12

咱们来看看ECMA官方文档对转number类型的说明:

转number类型
另外 再补充一点,在JS世界里,只有五种转Boolean类型是 false的: 0 Nan undefined null "" false。其余的转Boolean值都是 true。 咱们再来看看ECMA官方文档对转Boolean类型的说明:
转Boolean类型
因此,从这里咱们就能够发现其实原文的ECMA官方文档就是很棒的学习资料,已经帮你整理的很完备了。多去翻翻这些官方文档的资料颇有帮助。

例子1:属于基本类型间的比较,并且都是基本类型中的number类型,相同类型直接比较。

var a=1;
var b=1;
console.log(a == b); //true 

console.log("0" == "");   //false 
//都是相同的string类型,不用转,直接用字符串比较
复制代码

例子2:属于基本类型间的比较,可是其具体的类型不一样,须要转为number类型再去比较。

console.log(true == "true");  //false 相应转为number类型去比较:1与Nan比较
console.log(0 == "0");   //true 相应转为number类型去比较:0与0比较
console.log(0 == "");   //true 相应转为number类型去比较:0与0比较

console.log(undefined == null);  //true Nan与0比较??特殊
复制代码

例子3:属于三大特殊点

console.log(undefined == null);  //true 
console.log(undefined == 0);  //false 
console.log(null == 0);  //false 
console.log(Nan == Nan);  //false
复制代码

对象类型间的比较

对象间的比较中===(严格相等:值和类型都相等) 和 == 彻底同样。 规则:其实比较是否是同一个对象,比的就是他们的地址值是否同样。

例子1:对象类型间的比较

console.log({} === {});   //false 地址值不一样
console.log(new Number(1) == new Number(1));   //false 地址值不一样
复制代码

基本类型与对象类型间的比较

重点:这就是为啥以前引入valueOftoString() 的道理。 规则:把对象转成基本类型的数据以后再比 ?如何把对象转换成基本类型:1. 先调用这个对象(注意是对象)的valueOf()方法, 若是这个方法返回的是一个基本类型的值, 则用这个基本类型去参与比较。 2. 若是valueOf()返回的不是基本类型, 则去调用toString()而后用返回的字符串去参与比较。这个时候就是字符串与那个基本类型的比较,问题从而转为了基本类型间的比较。

例子1:

var p = {};
console.log(p.valueOf());  //{}
console.log(p == "[object Object]");  //true 
复制代码

解释:首先明确是对象与基本类型中的字符串比较;按照规则,先把对象调用其valueOf(),根据上节知识可知,返回的是this,也就是当前对象{}。不是基本数据类型,故再调用其toString(),返回"[object Object]",从而进行基本数据类型间的比较,根据规则,类型相同都是字符串,直接比较,故相等。

例子2:

var p1 = {
    valueOf : function (){
      return 'abc';
   },
    toString : function (){
      return {};
    }
  }
  console.log(p1 == "abc");  //true
复制代码

解释:首先明确是对象与基本类型中的字符串比较;按照规则,先把对象调用其valueOf(),根据上节知识可知,p有重写 valueOf,故直接输出字符串'abc',它属于基本数据类型,故再也不调用其toString()。进而进行基本数据类型间的比较,根据规则,类型相同都是字符串'abc',直接比较,故相等。

1.1.7 案例习题与面试题

案例1: 基本数据类型的判断

typeof返回数据类型的字符串(小写)表达

var a;
    console.log(a, typeof a, typeof a === 'undefined', a === undefined)  // undefined 'undefined' true true
    console.log(undefined === 'undefined'); //false(转为number实则是Nan与0的比较)
    a = 4;
    console.log(typeof a === 'number');  //true
    a = 'lvya';
    console.log(typeof a === 'string');  //true
    a = true;
    console.log(typeof a === 'boolean');  //true
    a = null;
    console.log(typeof a, a === null); // 'object' true
复制代码

案例2: 对象类型的判断

var b1 = {
        b2: [1, 'abc', console.log],
        b3: function () {
            console.log('b3');
            return function () {
                return 'ya Lv'
            }
        }
    };

    console.log(b1 instanceof Object, b1 instanceof Array); // true false
    console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) ;// true true
    console.log(b1.b3 instanceof Function, b1.b3 instanceof Object); // true true
    console.log(typeof b1.b2); // 'object'
    console.log(typeof b1.b3 === 'function');// true
    console.log(typeof b1.b2[2] === 'function'); // true
    b1.b2[2](4);  //4
    console.log(b1.b3()());  //ya Lv
复制代码

instanceof通常测对象类型,那它去测基本数据类型会出现怎样的奇妙火花呢?一块儿来验证一下。instanceOf内部的实现原理能够直接看3.2.3节。

//1并非Number类型的实例
	console.log(1 instanceof Number);  //false
	//new Number(1)的确是Number类型的实例
    console.log(new Number(1) instanceof Number);  //true
复制代码

面试3: 考察typeOf检测数据类型

typeof来测试有如下七种输出结果:'number' 'string' 'boolean' 'object' 'function' 'symol' 'undefined'。注意都是字符串表达方式。

console.log(typeof "ab");    // string
console.log(String("ab"));   //'ab' 能够知道String("ab")就是var s='ab'的含义
console.log(typeof String("ab"));  // string
console.log(typeof  new String("ab")); // object
console.log(typeof /a/gi);    // object
console.log(typeof [0,'abc']);  // object
console.log(typeof function (){});  //function
var f = new Function("console.log('abc')");
f();  //'abc' 能够知道f就是一个函数
console.log(typeof f);   //function
console.log(typeof  new Function("var a = 10"));  //function
复制代码

面试4: 考察+的运用

JS加号有两种用法: Case 1:数学上的加法(只要没有字符串参与运算就必定是数学上的数字): Case 2:字符串链接符(只要有一个是字符串,那就是字符串连接)

console.log(1 +  "2" + "2"); // 122
console.log(1 +  +"2" + "2"); // 32 (这里+'2'前面的加号是强转为number的意思)
console.log(1 +  -"1" + "2"); // 02
console.log(+"1" +  "1" + "2"); // 112
console.log( "A" - "B" + "2"); // NaN2
console.log( "A" - "B" + 2); // NaN
复制代码

面试5: 考察valueOftoString

console.log([] == ![]); //true
复制代码

说明:首先左边是[],右边是![]这个是一个总体,由1.1.6节知识可知,世界上只有五种转Boolean值得是false,其余都是true。故右边这个![]总体结果是false。综上,明确这是对象与基本类型(布尔值)的比较。 而后,就是将对象先调用valueOf后调用toString的规则去判断,由1.1.6节可知,左边是对象,首先用valueOf 返回的是一个数组对象(注意若是是{}valueOf()就是返回this,此时this{}!) 而后再调用toString 返回一个空的字符串,由于数组转字符串,就是去掉左右“中括号”,把值和逗号转为字符串,看一下验证:

console.log([].valueOf()); //[]
console.log([].valueOf().toString()); //空的字符串
复制代码

故左边是一个空的字符串。右边是false。又转为基本数据间的比较,两个不一样类型,则转为number类型去比较。 空字符串转number为0,falsenumber为0。故0==0结果就是true

面试6: && ||的短路现象

&& ||在js中一个特别灵活的用法。若是第一个能最终决定结果的,那么结果就是第一个值,不然就是第二个。这个在实际项目中使用也很常见。 与和或的优先级:"与" 高于 "或",也就是&& 优先级大于 ||

console.log(1 && 2 || 0); // 2
console.log((0 || 2 && 1)); //1 (注意,这里是先计算2 && 1,由于&&优先级高于||)
console.log(3 || 2 && 1); // 1 (注意,这里是先计算2 && 1,由于&&优先级高于||)
console.log(0 && 2 || 1); // 1
复制代码

面试7: 类型转换综合题

  1. +当作数字相加,由于两边都没字符串,故都转number

    var bar = true;
    console.log(bar + 0);  // 1 
    复制代码
    var bar = true;
    console.log(bar + true); // 2
    复制代码
    var bar = true;
    console.log(bar + false); // 1
    复制代码
    var bar = true;
    console.log(bar + undefined); // Nan
    复制代码
    var bar = true;
    console.log(bar + null); // 1
    复制代码
    console.log(undefined + null); // Nan (Nan与任何运算结果都是Nan)
    复制代码
  2. +当作字符串链接,由于有一个为字符串

    var bar = true;
    console.log(bar + "xyz"); // truexyz 
    复制代码
  3. 隐含的类型转换

    console.log([1, 2] + {});  //1,2[object Object]
    复制代码
    Array.prototype.valueOf = function () {
      return this[0];
    };
    console.log([1, 2] + [2]);  //3 
    /**重写了Array的valueOf方法,其重写后返回的是this[0], 由于在这是number类型1,故直接用。*/
    复制代码
    console.log([{}, 2] + [2]);  // [object Object],22 
    /**重写了Array的valueOf方法,其重写后返回的是this[0], 由于在这是一个对象{},故从新在对这个数组对象([{},2])调用toString()返回‘[object Object],2’。 这里要注意当调用toString是整个对象,而非重写valueOf后返回来的对象。 +右边的[2]是调用了valueOf以后返回的number类型2,因此直接用, 由于左边是一个字符串,因此加号表明字符串拼接。返回最终结果[object Object],22 */
    复制代码

1.2 数据,变量, 内存的理解

1.2.1 什么是数据?

  • 存储在内存中特定信息的"东东",本质上是0101...的二进制

  • 数据的特色:可传递, 可运算

    var a = 3;
    var b = a;
    复制代码

    这里体现的就是数据的传递性。变量a是基本数据类型,保存的值是基本数据类型number值为3。在栈内存中存储。这两个语句传递的是变量a吗?不是。传递的是数据3。实际上,是拿到变量a的内容数字3拷贝一份到b的内存空间中

注意:无论在栈内存空间保存的基本数据类型仍是在堆内存中保存的对象类型,这些内存都有地址值。只是要不要用这个地址值的问题。对象的地址值通常会用到。因此不少人会误觉得只有对象才有地址值,这是错误的理解。

  • 在内存中的全部操做的目标是数据
    • 算术运算(加减乘除)
    • 逻辑运算(与或非)
    • 赋值(=)
    • 运行函数(例如执行fn(),此时()就是能够看作是一种操做数据的方式,去执行代码块)

1.2.2 什么是变量?

  • 在程序运行过程当中它的值是容许改变的量,由变量名和变量值组成
  • 一个变量对应一块小内存,它的值保存在这个内存中。变量名用来查找对应的内存, 变量值就是内存中保存的数据。经过变量名先去找到对应的内存,而后再去操做变量值。

1.2.3 什么是内存?

  • 内存条通电后产生的可存储数据的空间(临时的),它是临时的,但处理数据快

  • 硬盘的数据是永久的,但其处理数据慢

  • 内存产生和死亡: 内存条(电路版) -> 通电 -> 产生内存空间 -> 存储数据 -> 处理数据 -> 断电 -> 内存空间和数据都消失

  • 内存空间的分类:

    • 栈空间: 全局变量和局部变量【空间比较小】

    • 堆空间: 对象 (指的是对象(函数也是对象)自己在堆空间里,其自己在堆内存中。但函数名在栈空间里。)【空间比较大】

      //obj这个变量在栈空间里 name是在堆空间里
      function fn () {
          var obj = {name: 'lvya'}
        }
      复制代码
  • 一块小的内存包含2个方面的数据

    • 内部存储的数据(内容数据)

    • 地址值数据(只有一种状况读的是地址值数据,那就是将一个对象给一个变量时)

      var obj = {name: 'lvya'} ;
      var a = obj ;
      console.log(obj.name) ;
      复制代码

      执行var obj = {name: 'Tom'} 是将右边的这个对象的地址值给变量obj,变量obj这个内存里面保存的就是这个对象的地址值。 而var a = obj 右边不是一个对象,是一个变量(引用类型的变量),把obj的内容拷贝给a,而恰好obj的存储的内容是一个对象的地址值。 执行console.log(obj.name) 读的是obj.name的内容值。 总结:何时读的是地址值?只有把一个对象赋值给一个变量时才会读取这个对象在内存块中的地址值数据。上述三条语句只有var obj = {name: 'Tom'}才是属于读地址值的状况。

      内存结构图

1.2.4 内存,数据, 变量三者之间的关系

  • 内存是容器, 用来存储不一样数据
  • 变量是内存的标识, 经过变量咱们能够操做(读/写)内存中的数据

1.2.5 一些相关问题

问题一:var a = xxx, a内存中到底保存的是什么?
须要分类讨论:

  • 当xxx是基本数据, 保存的就是这个数据

    var a = 3//3是基本数据类型,变量a保存的就是3.
    复制代码
  • 当xxx是对象, 保存的是对象的地址值

    a = function () {
        }
    //函数是对象,那么a保存的就是这个函数对象的地址值。
    复制代码
  • 当xxx是一个变量, 保存的xxx的内存内容(这个内容多是基本数据, 也多是地址值)

    var b = 'abc'
      a = b
      //b是一个变量,而b自己内存中的内容是一个基本数据类型。
      //因此,a也是保存这个基本数据类型'abc'
    复制代码
    b = {}
      a = b
      //b是一个变量,而b自己内存中的内容是一个对象的地址值。
      //因此,a也是保存这个对象的地址值'0x123'
    复制代码

问题二:关于引用变量赋值问题?

  • 2个引用变量指向同一个对象, 经过一个变量修改对象内部数据, 另外一个变量看到的是修改以后的数据

    var obj1 = {name: 'Tom'}
    var obj2 = obj1
    obj2.name = 'Git'
    console.log(obj1.name)  // 'Git'
    
    function fn (obj) {
      obj.name = 'A'
    }
    fn(obj1)
    console.log(obj2.name) //A
    复制代码

    执行var obj2 = obj1 obj1是一个变量,而非对象。故把obj1的内容拷贝给obj2,只是恰好这个内容是一个对象的地址值。这个时候,obj1 obj2 这两个引用变量指向同一个对象{name: 'Tom'}。 经过其中一个变量obj2修改对象内部的数据。 obj2.name = 'Git' 那么这个时候,另一个对象看到的是修改后的结果。固然,后面的对fn(obj1),也是同样的操做,涉及到实参和形参的赋值。

  • 2个引用变量指向同一个对象, 让其中一个引用变量指向另外一个对象, 另外一引用变量依然指向前一个对象。

    var a = {age: 12};
    var b = a;
    a = {name: 'BOB', age: 13};
    b.age = 14;
    console.log(a.name, a.age,b.age);// 'BOB' 13 14
    function fn2 (obj) {
      obj = {age: 100}
      console.log(obj.age);  //100
    }
    fn2(a);   //函数执行完后会释放其局部变量
    console.log(a.age,b.age)  //13 14
    console.log(obj.age);  //报错 obj is not defined
    复制代码

    一开始两个引用变量a b 都指向同一个对象,然后执行a = {name: 'BOB', age: 13};语句,就是让a指向另外一个对象{name: 'BOB', age: 13}a中的内容的地址值变化了。而b仍是指向以前a的那个对象{age: 12}这个例子要好好理解,看图解:未执行fn2函数以前和执行fn2函数后

    js
    js

问题三:在JS调用函数时传递变量参数时,是值传递仍是引用传递?

  • 理解1: 都是值(基本类型的值/对象的地址值)传递

  • 理解2: 多是值传递, 也多是引用传递(这个时候引用传递理解为对象的地址值)

    var a = 3
       function fn (a) {
         a = a +1
       }
       fn(a)
       console.log(a)   //3
    复制代码

    fn(a)中的a是一个实参。function fn (a)中的a是一个形参。 var a = 3中的a是一个全局变量,其内存中存储的内容是基本数据类型3。实参与形参的传递是把3传递(拷贝)给形参中的a的内存中的内容。传递的不是a!而是3。而后,执行完以后,函数里面的局部变量a被释放,当输出a值时,确定去读取全局变量的a。传递的是值3。

    function fn2 (obj) {
    console.log(obj.name)   //'Tom'
     }
     var obj = {name: 'Tom'}
     fn2(obj)
    复制代码

    fn2(obj)中的实参obj(引用变量)把其内容(恰好是地址值)传递给形参中的内容。而不是指把{name: 'Tom'}整个对象赋值给形参obj。是把地址值拷贝给形参obj。也就是实参obj 形参obj这两个引用变量的内容同样(地址值同样)。传递的是地址值。

问题四:JS引擎如何管理内存?

  1. 内存生命周期 分配小内存空间, 获得它的使用权 存储数据, 能够反复进行操做 释放小内存空间

  2. 释放内存 局部变量:函数执行完自动释放(全局变量不会释放内存空间) 对象:成为垃圾对象==>垃圾回收器回收(会有短暂间隔)

    var a = 3
    var obj = {}
    //这个时候有三块小的内存空间:第一块 var a = 3 第二块 var obj 第三块 {} 在堆内存中
    obj = undefined
    //无论obj = null/undefined 这个时候内存空间还有两块。没有用到的{}会有垃圾回收器回收。
    function fn () {
      var b = {}
      //b局部变量 整个局部变量的生命周期是在函数开始执行到结束。
    }
    fn() // 局部变量b是自动释放, b所指向的对象是在后面的某个时刻由垃圾回收器回收
    复制代码

1.3 对象的理解和使用

这一节主要是对对象的基本理解和使用做出阐述,一些基本的问题笔者会简单地在Part 1这部分罗列出来。具体的深刻问题在Part 2中深刻探讨。那么在了解对象的概念时,思想很重要,那就是对象是如何产生的?对象内部有啥须要关注?至于对象如何产出,有new出来,字面量定义出来的,函数return出来的等等状况。至于内部有啥呢,主要关注就是属性和方法这两个东西。

什么是对象?

  • 变量能够存数据,对象也能够存数据,那么他与变量功能就会有差别。
  • 对象是多个数据(属性)的集合
  • 也能够说对象是用来保存多个数据(属性)的容器
  • 一个对象就是描述咱们生活中的一个事物。

为何要用对象? 统一管理多个数据。若是不这么作,那么就要引入不少的变量。 好比我如今要创建一个对象Person,里面有name age gender等等,我能够用一个对象去创建数据容器,就不须要单独设置不少变量了。方便管理。

对象的分类?

  • 内建对象---在任何ES实现中均可以使用(不需new,直接引用)好比Math/String/Function/Number/Data
  • 宿主对象---由JS运行环境提供对象(浏览器提供)全部的BOM和DOM对象都是宿主对象。好比console.log() document.write()
  • 自定义对象---由开发人员建立

对象的组成?

  • 属性: 属性名(字符串)和属性值(任意类型)组成-----描述对象的状态数据
  • 方法: 一种特别的属性(属性值是函数)-----描述对象的行为数据
  • 它们之间的联系就是方法是特殊的属性。

在了解完对象以后,咱们知道每一个对象会去封装一些数据,用这个对象去映射某个事物。那么这些数据就是由属性来组成的,如今咱们看看属性的一些相关知识。

属性组成?

  • 属性名 : 字符串(标识),本质上是字符串。本质上属性名是加引号的,也就是字符串。但通常实际操做都不加。

  • 属性值 : 任意类型(因此会有方法是特别的属性这一说法。)

    属性名本质上是字符串类型,见下例:

    var obj={
          'name':'猪八戒';
          'gender':'男';
      }
    复制代码

    上例子通常咱们不会特地这样去将属性名打上双引号,咱们通常习惯这样写:

    var obj={
          name:'猪八戒';
          gender:'男';
      }
    复制代码

    再看一道对象属性名知识点的面试题:

    var a = {},
        b = {key: 'b'},
        c = {key: 'c'};
        a[b] = 123;  // a["[object Object]"] = 123
        a[c] = 456;  // a["[object Object]"] =456
        console.log(a[b]); //输出456 求a["[object Object]"]=?
    复制代码

    上例解释:属性名本质上是字符串。ES6以前对象的属性名只能是字符串, 不能是其余类型的数据! 若是你传入的是其余类型的数据做为属性名, 则会把其余类型的数据转换成字符串,再作属性名。如果对象,那么就调用toString(),ES6 属性名能够是Symbol类型。 再看一个例子:

    var a = {
          "0" : "A",
          "1" : "B",
          length : 2
      };
      for(var i = 0; i < a.length; i++){//a是对象,a.length是读取对象的属性,为2.
          console.log(a[i]);
      }
      //会输出A B
    复制代码

    再看一个例子:

    var a = {};
    a[[10,20]] = 2000; 
    //首先把握好a是对象,a[]就是使用对象读其属性的语法,而不是数组。把a[]中[10,20]本质上是字符串,因此要转啊。
    //[10,20]转字符串就是对象转字符串,调用toString(),变成“10,20”。这个转的字符串就是属性名。
    console.log(a);  // 输出 {10,20: 2000}
    复制代码

属性的分类?

  • 通常 : 属性值不是function ,描述对象的状态
  • 方法 : 属性值为function的属性 ,描述对象的行为

数组和函数是特别的对象?

  • 数组: 属性名是0,1,2,3之类的索引(有序)
  • 函数: 能够执行的

如何访问对象内部的数据 ?

  • .属性名: 编码简单, 有时不能用。
  • ['属性名'] :编码麻烦, 能通用。
var p = {
      name: 'Tom',
      age: 12,
      setName: function (name) {
        this.name = name
      },
      setAge: function (age) {
        this.age = age
      }
    };

    p.setName('Bob')  //用.属性名的方式
    p['setAge'](23)   //用['属性名']语法
    console.log(p.name, p['age'])  //Bob 23 
复制代码

何时必须使用['属性名']的方式?

  1. 属性名包含特殊字符: - 或 空格
  2. 属性名不肯定时。
var p = {};
    //1. 给p对象添加一个属性: content type: text/json
    // p.content-type = 'text/json' //不能用
    p['content-type'] = 'text/json'
    console.log(p['content-type'])

    //2. 属性名不肯定,用变量去存储这个值。
    var propName = 'myAge'
    var value = 18
    // p.propName = value //不能用
    p[propName] = value   //propName表明着的就是一个变量
    console.log(p[propName])   //18
复制代码

函数对象(Function Object)是什么呢?

  • 函数做为对象使用的时候,这个函数就是函数对象。

1.4 函数的理解和使用

其实在JavaScript中笔者认为最复杂的数据类型,不是对象,而是函数。为何函数是最复杂的数据类型呢?由于函数能够是对象,它自己就会有对象的复杂度。函数又能够执行,它有不少的执行调用的方式(这也决定了函数中this是谁的问题),因此他又有函数执行的复杂度。这一小节咱们就简单来讲说函数的基本知识。在Part3会去更深刻去介绍JS中的函数。

什么是函数?

  • 定义:用来实现特定功能的, n条语句的封装体,在须要的时候执行此功能函数。
  • 注:只有函数类型的数据是能够执行的, 其它的都不能够

为何要用函数?

  • 提升复用性(封装代码)

  • 便于阅读交流

    function showInfo (age) {
          if(age<18) {
            console.log('未成年, 再等等!')
          } else if(age>60) {
            console.log('算了吧!')
          } else {
            console.log('恰好!')
          }
        }
        //若是不用函数作,也能够,但要把中间的代码书写不少遍。
        //而函数就是抽象出共同的东西,把这些执行过程封装起来,给你们一块儿用。
        showInfo(17)  //未成年, 再等等!
        showInfo(20)  //恰好!
        showInfo(65)  //算了吧!
    复制代码

如何定义函数 ?

  • 函数声明

  • 表达式

  • 建立函数对象 var fun = new Function( ) ; 通常不使用

    function fn1 () { //函数声明
          console.log('fn1()')
        }
        var fn2 = function () { //表达式
          console.log('fn2()')
        }
        fn1();
        fn2();
    复制代码

如何调用(执行)函数?

  • test(): 直接调用

  • obj.test(): 经过对象去调用

  • new test(): new调用

  • test.call/apply(obj): 临时让test成为obj对象的方法进行调用

    var obj = {}  //一个对象
    function test2 () {  //一个函数
      this.xxx = 'lvya'
    }
    // obj.test2() 不能直接, 根本obj对象中就没有这样的函数(方法)
    test2.call(obj) // 至关于obj.test2() , 可让一个函数成为指定任意对象的方法进行调用 
    console.log(obj.xxx)  //lvya
    //这个借调是JS有的,其余语言作不到。借调就是假设一个对象中没有一个方法,
    //那么就可让这个方法成为想要调用这个方法的对象去使用的方式。
    //也就是一个函数能够成为指定任意对象的方法进行调用 。
    复制代码

函数也是对象

  • instanceof Object===true
  • 函数有属性: prototype
  • 函数有方法: call()/apply()
  • 能够添加新的属性/方法

函数的3种不一样角色

  • 通常函数 : 直接调用
  • 构造函数 : 经过new调用
  • 对象 : 经过对象.调用内部的属性/方法

this是什么?

  • 任何函数本质上都是经过某个对象来调用的,若是没有直接指定就是window。
  • 全部函数内部都有一个变量this,它的值是调用函数的当前对象
  • <具体this总结见Part2部分>

如何肯定this的值?

  • test(): window

  • p.test(): p

  • new test(): 新建立的对象

  • p.call(obj): obj

  • 回调函数: 看背后是经过谁来调用的: window/其它

    <script type="text/javascript">
        function Person(color) {
          console.log(this)
          this.color = color;
          this.getColor = function () {
            console.log(this)
            return this.color;
          };
          this.setColor = function (color) {
            console.log(this)
            this.color = color;
          };
        }
    
        Person("red"); //this是谁? window
    
        var p = new Person("yello"); //this是谁? p(Person)
    
        p.getColor(); //this是谁? p (Person)
    
        var obj = {};
        p.setColor.call(obj, "black"); //this是谁? obj (Object)
    
        var test = p.setColor;
        test(); //this是谁? window
    
        function fun1() {
          function fun2() {
            console.log(this);
          }
    
          fun2();
        }
        fun1();  //this是谁? window
      </script>
    复制代码

匿名函数自调用:

(function(w, obj){
    //实现代码
  })(window, obj)
复制代码
  • 专业术语为: IIFE (Immediately Invoked Function Expression) 当即调用函数表达式
  • 做用
    • 隐藏实现 (让外部的全局看不到里面)
    • 不会污染外部(全局)命名空间
    • 用它来编码js模块
;(function () { //匿名函数自调用
      var a = 1
      function test () {
        console.log(++a)
      }
      window.$ = function () { // 向外暴露一个全局函数
        return {
          test: test
        }
      }
    })()

    $().test() //需明白 1. $是一个函数 2. $执行后返回的是一个对象 3.而后对象.方法()执行函数。
复制代码

回调函数的理解

  • 什么函数才是回调函数?
    • 你定义的
    • 你没有调用
    • 但它最终执行了(在必定条件下或某个时刻)
  • 经常使用的回调函数
回调函数类型 this是指向谁?
DOM事件回调函数 发生事件的DOM元素
定时器回调函数 Window
ajax请求回调函数 Window
生命周期回调函数 组件对象

函数中的arguments 在调用函数时,浏览器每次都会传递两个隐含的参数:

  1. 函数的上下文对象 this
  2. 封装实参的对象 arguments(类数组对象)。这里的实参是重点,就是执行函数时实际传入的参数的集合。
function foo() {
	      console.log(arguments);  //Arguments(3)返回一个带实参数据的类数组
	      console.log(arguments.length);  //3 类数组的长度
	      console.log(arguments[0]);   //ya LV 能够不传形参,能够访问到实参
	      console.log(arguments.callee);  // ƒ foo() {...} 返回对应当前正在执行函数的对象
	    }
	    foo('ya LV',18,'male');
复制代码

arguments妙用1:利用arguments实现方法的重载

  • a.借用arguments.length属性来实现

    function add() {
            var len = arguments.length,
                sum = 0;
            for(;len--;){
                sum += arguments[len];
            }
            return sum;
        }
    
        console.log( add(1,2,3) );   //6
        console.log( add(1,3) );     //4
        console.log( add(1,2,3,5,6,2,7) );   //26
    复制代码
  • b.借用prototype属性来实现

    function add() {  
          return Array.prototype.reduce.call(arguments, function(n1, n2) {  
            return n1 + n2;  
          });     
        };  
        add(1,2,3,6,8);   //20
        //三个经常使用的数组的高阶函数:map(映射)filter(过滤)reduce(概括)
        //能够参见ES6函数新增特性之箭头函数进一步优化
    复制代码

arguments妙用2.利用arguments.callee实现递归

先来看看以前咱们是怎么实现递归的,这是一个计算阶乘的函数:

function factorial(num) { 
	       if(num<=1) { 
	           return 1; 
	       }else { 
	           return num * factorial(num-1); 
	       } 
	   } 
复制代码

可是当这个函数变成了一个匿名函数时,咱们就能够利用callee来递归这个函数。

function factorial(num) { 
        if(num<=1) { 
            return 1;  //若是没有这个判断,就会内存溢出
        }else { 
            return num * arguments.callee(num-1); 
        } 
    } 
     console.log(factorial(5));  //120
复制代码

1.5 补充

1.5.1 分号问题

  • js一条语句的后面能够不加分号,相似“能够加分号可是你们都不加” 的语言就有:Go, Scala, Ruby, Python, Swift, Groovy...

  • 是否加分号是编码风格问题, 没有应该不该该,只有你本身喜欢不喜欢

  • 在下面2种状况下不加分号会有问题

    小括号开头的前一条语句

    var a = 3
      ;(function () {
      })()
    
    //若是不加分号就会这么错误解析:
    // var a = 3(function () {
    // })();
    复制代码

    中方括号开头的前一条语句

    var b = 4
          ;[1, 3].forEach(function () {
          })
    
        // 若是不加分号就会这么错误解析:
        // var b = 4[3].forEach(function () {
        // })
    复制代码
  • 解决办法: 在行首加分号

  • 强有力的例子: Vue.js库。Vue.js 的代码所有不带分号。

  • 有一个工具全自动帮你批量添加或者删除分号:工具地址

1.5.2 位运算符和移位在JS中的操做

像二进制,八进制,十进制,十六进制这些概念在JavaScript中不多被体现出来,但是笔者以为这个是码农的素养,因此我以为有必要再去搞懂。另一个就是原码反码补码的概念。好比在计算机硬件电路中有加法器,全部的运算都会转为加法运算,减法就是用加法来实现。因此才引出原码反码补码的概念去解决这一问题。

那么笔者如今着重讲一下位运算符操做和移位操做。js中位运算符有四种:按位取反(~)、按位与(&)、按位或(|)、按位异或(^)。移位操做有四种:带符号向右移动(>>)、无符号向右移动(>>>)、带符号向左移动(<<)、无符号向左移动(<<<).

示例1:如何快速判断一个数是否是奇数?

那么,取余是你先想到的,那么还有其余方法吗?就是用位运算符去解答。先思考奇数3(二进制是11),偶数4(二进制是100),可知偶数的最低位为0,奇数的最低位为1,那么咱们只要经过某种方法获得一个数的二进制的最低位,判断它是否是为1,是1那这个数就是奇数。

如今的问题就变成了,怎么获得一个数的二进制最低位呢?那就是用按位与1(& 1)去作。假设一个数的二进制为1111 1111 那么只要按位与1(1的二进制为0000 0001)是否是前面一排“与0”都变成0了,只剩最低位了,这样目标数与1的按位与运算的结果就是等价于目标数的二进制最低位。

var num = 57 ;
    if(num & 1){
        console.log(num + "是奇数");  //57是奇数
    }else{
        console.log(num + "是偶数");
    }
复制代码

示例2:怎么交换两个number类型的变量值?

新增一个变量来存储这种方式是你先想到的,那么另一种就是经过按位异或操做去交换变量。

异或就是不一样的为true(1),相同的为false(0)。

10^10=0 由于1010 ^ 1010 = 0000

11^0=11 由于1011 ^ 0000=1011

因此获得两个结论:

第一,两个相同的number数异或为0;第二,任何number数与0异或是其自己。

var a = 10;
var b = 20;
a = a ^ b;  //a=10 ^20
b = a ^ b;  //b=10 ^20^20 =10 ^(20^20)=10^0=10 
a = a ^ b;  //a=10 ^20^ 10 =(10^10)^20=0^20=20
console.log(a, b);  //20 10 -交换变量成功-

//但这种方法只适用于number数据类型。
//另外能够用ES6中的数组解构。
复制代码

示例3:如何计算出一个数字某个二进制位?

在回答这个问题前,咱们先总结出一些结论供咱们使用。移位都是当作32位来移动的,但咱们这里就简单操做,用8位来模拟。

先看带符号向右移位:

10 >> 1 翻译题目:10带符号向右移动一位是几?

0000 1010 >> 1

0000 0101 这个结果就是移位后的结果。咱们能够知道0101就是十进制的5.

带符号向右移动就是总体向右移动一位,高位用符号位去补。正数用0补,负数用1补。

咱们能够看出结论,带符号向右移动其实就是往右移动一位,至关于除以2.

如今再来看看带符号向左移位:

10 << 2 翻译题目: 10带符号向左移动2位是几?

0000 1010 << 2

0010 1000 低位用0补。这个 0010 1000 就是数就是40.

咱们能够看出结论,带符号向左移动其实就是往左移动一位,至关于乘以2。移动2位便是乘4。

如今回归题目,假设我要知道10(1010)的倒数第三位的0这个进制位。

首先往右移动两位变成0010 , 而后进行 ‘&1’ 操做 , 0010 & 0001 =0000 =0 ,这个0就是10的二进制位的倒数第三位。因此是经过:(10 >> 2 & 1)的方式获得10的倒数第三位的进制位。

示例4:如何计算出2的6次方最快算法?

2B程序猿会用2 * 2 * 2 * 2 * 2 * 2的方法吧。码农可能会用for循环去作或者用Math.pow(2,6)去写。

可是这些都不是最快的。咱们来看看高级工程师会怎么写,哈哈。咱们刚刚获得过2个结论,其中一个就是带符号向左移位其实就是往左移动一位,至关于乘以2。 移动2位,就是乘4。"左乘右除"。那么如今我是否是能够对 1 移动6位 不就能够了吗?因此就一行代码:1 << 6 。

由汇编知识咱们知道,移位是最底层的计算。能够彻底用加法器实现。而Math.pow(2,6)其实会有不少的汇编指令才能够实现这一条代码。但1 << 6 只须要一条,因此,性能是很好的。

1.5.3 内存溢出与内存泄露

内存溢出:

  • 一种程序运行出现的错误

  • 当程序运行须要的内存超过了剩余的内存时, 就出抛出内存溢出的错误。

    // 1. 内存溢出
        var obj = {}
        for (var i = 0; i < 10000; i++) {
          obj[i] = new Array(10000000)
          console.log('-----')
        }
      //直接崩掉了,须要的内存大于目前空闲的内存,直接报错误:内存不足。
      //就如一个水杯,水倒满了就溢出,这就是内存溢出。
    复制代码

内存泄露:

  • 占用的内存没有及时释放,这时程序仍是能够正常运行的

  • 内存泄露积累多了就容易致使内存溢出

  • 常见的内存泄露:

  1. 意外的全局变量

    // 在乎外的全局变量--在ES5的严格模式下就会报错。
      function fn() {
        a = new Array(10000000)
        console.log(a)
      }
      fn()
    //a就是意外的全局变量,一直会占着内存,关键它仍是指向一个数组很是大的对象。这块内存就一直占着。
    复制代码
  2. 没有及时清理的计时器或回调函数

    // 没有及时清理的计时器或回调函数
      var intervalId = setInterval(function () { //启动循环定时器后不清理
        console.log('----')
      }, 1000)
    
      // clearInterval(intervalId)
    复制代码
  3. 闭包

    // 闭包
      function fn1() {
          var a = 4
          function fn2() {
            console.log(++a)
          }
          return fn2
        }
        var f = fn1()
        f()
        //f指向的fn2函数对象一直都在,设f指向空对象,进而让fn2成为垃圾对象,进而去回收闭包。
        // f = null
    复制代码

1.5.3 函数节流与函数防抖

函数节流:让一段程序在规定的时间内只执行一次

<script type="text/javascript">
    window.onload = function () {
      // 函数节流: 让一段程序在规定的时间内只执行一次
      let flag = false;
      document.getElementsByTagName('body')[0].onscroll = function () {
        if(flag){
          return;
        }
        flag = true; //当以前为FALSE的时候进来,我在2s内才会有定时器注册。
        setTimeout(function () {
          console.log('滚动过程当中,2s只会注册定时器一次');
          flag = false; //一次后,为第二次作准备。只要是TRUE,我就进不来注册定时器的逻辑
        }, 2000)
      }
    }
</script>
复制代码

函数防抖: 让某一段程序在指定的事件以后触发

<script type="text/javascript">
//场景:让滚动完以后2s触发一次。
    window.onload = function () {
      
      // 函数防抖: 让某一段程序在指定的事件以后触发
      let timeoutId = null;
      document.getElementsByTagName('body')[0].onscroll = function () {
        timeoutId && clearTimeout(timeoutId); //当第一次来的时候为null,不须要清除定时器。
        timeoutId = setTimeout(function () {
          console.log('xxx');
        }, 2000)   //在这个滚动过程当中的最后一次才注册成功了,其余的定时器都注册完后立刻清除。
      }
    }
</script>
复制代码

此文档为吕涯原创,可任意转载,但请保留原连接,标明出处。 文章只在CSDN和掘金第一时间发布: CSDN主页:https://blog.csdn.net/LY_code 掘金主页:https://juejin.im/user/5b220d93e51d4558e03cb948 如有错误,及时提出,一块儿学习,共同进步。谢谢。 😝😝😝