JS基础知识深入总结

2,418 阅读31分钟

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

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

暂时会对以上四个专题去总结,现在开始JS之旅的第一部分:JS基础知识深入总结。下图是我这篇的大纲。 js基础大纲前端学习大纲

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

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不能去判断出objectarray

  • ===(全等符号) 注1:只可以判断undefined 和 null 因为这两种基本类型的值是唯一的,即可用全等符比较。

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

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

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

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 false0
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主页:blog.csdn.net/LY_code
掘金主页:juejin.im/user/366762…
若有错误,及时提出,一起学习,共同进步。谢谢。 😝😝😝