JS语言精粹

小小知识点:

  1. typeof是一个操作符,并不是function。所以typeof后面不用加括号。
    参见MDN


    JS语言精粹_第1张图片
    image.png
  2. 关于 typeof null // Object
    null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回"object"的原因,如

  3. 只声明不赋值,则默认值为undefined

  4. 关于const

     const num = 1;
     num = 2; //Uncaught TypeError: Assignment to constant variable.
    
     const obj = {
       data: 1
     }
     obj.data = 2;//没有报错
    

    咦,不是说const定义的常量不能被改变吗?
    const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,除非我们直接重写对象也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。所以

     const obj = {
         data:1
     }
    
     obj = {
         data:4
     }//Uncaught TypeError: Assignment to constant variable.
    
  5. 关于arguments,我们更推荐这么使用
    这时的args就是真正的数组了。

     function foo(...args){
         return args;
     }
     foo(1,2,3)//[1,2,3]
    
  6. 函数的返回值有两种结果:
    显示调用return 返回 return 后表达式的求值
    没有调用return 返回 undefined

  7. 关于this
    在普通函数中
    严格模式下,this指向undefined
    非严格模式下,this指向全局对象(Node中的global,浏览器中的window)
    在ES6中默认使用严格模式,所以在普通函数中你会发现this指向undefined。

  8. 语句和表达式
    JavaScript是一种语句优先的语言
    逗号运算符的左右两侧都必须是表达式!如果是a:1,那肯定会报错。

逗号表达式的一般形式是:表达式1,表达式2,表达式3……表达式n
逗号表达式的求解过程是:先计算表达式1的值,再计算表达式2的值,……一直计算到表达式n的值。最后整个逗号表达式的值是表达式n的值。
看下面几个例子:
x=8*2,x*4 /整个表达式的值为64,x的值为16/
(x=8*2,x*4),x2 /整个表达式的值为128,x的值为16/
x=(z=5,5*2) /
整个表达式为赋值表达式,它的值为10,z的值为5/
x=z=5,5*2 /
整个表达式为逗号表达式,它的值为10,x和z的值都为5*/
逗号表达式用的地方不太多,一般情况是在给循环变量赋初值时才用得到。所以程序中并不是所有的逗号都要看成逗号运算符,尤其是在函数调用时,各个参数是用逗号隔开的,这时逗号就不是逗号运算符。

有时候会遇到一些很奇怪的错误,就尽量往语句优先这个方向想。

  1. 立即执行函数

    function(){}() //Uncaught SyntaxError: Unexpected token (
    这里为什么会报错呢?如果不加第一对括号,无论是

     function(){
        /* 代码 */
     }();
    

    还是

     function(){
         /* 代码 */
     }
    

    都是会报错的。因为js的引擎会把这里的 function 看成是函数声明,而函数声明不允许没有函数名,因此会对匿名函数报错。

    匿名函数只允许以表达式的形式存在,例如:

     setTimeout(function(){
         /* 代码 */
     }, 1000);
    

    这里的匿名函数就是作为 setTimeout 的一个参数,是表达式,这种写法是允许的。

    或者:

     var foo = function(){
         /* 代码 */
     };
    

    也就是说我们稍微修改一下,var fn = function(){}(),也不会报错。
    这是为什么?
    因为这是把一个匿名函数赋值给一个变量,匿名函数在该语句中充当函数表达式的角色。

    如果这里的函数有名字呢?不会报错,但语义会发生变化。例如:

     function foo(x){
         /* 代码 */
     }(1);
    

    其实这里的代码就相当于

     function foo(x){
         /* 代码 */
     };
     (1);
    

    原因是js引擎会认为前面的函数是一个函数声明的语句,而后面的(1)是另一个单独的语句,于是执行后面的语句,在控制台输出1。

    js的括号有几种不同的作用,其中一种作用就是:表示在括号内的是表达式而不是语句。具体到这个例子上,第一对括号就是告诉js引擎,这里面的匿名函数是一个函数表达式,而不是函数声明语句。因此加了这个括号之后,就不会报错了。
    (function(){})() //强制其理解为函数,()里面放表达式,“函数()”表示执行该函数,即声明后立即执行了。

  2. 补充原型链的一个小知识:
    每个原型对象prototype中都有一个constructor属性,默认指向函数本身。

  3. 闭包和内存泄露的问题
    首先应当明确的是内存泄漏可能是代码的问题,而不是闭包的问题,如果为了避免内存泄漏而不去用闭包的话,有些问题是很难解决的。

在IE9之前,BOM和DOM对象都是使用C++以COM对象的形式来实现的,COM对象的垃圾收集机制采用的就是计数策略,所以IE中只要涉及到BOM和DOM对象,就很可能会存在循环引用的问题。

闭包实际上很容易造成JavaScript对象和DOM对象的隐蔽循环引用。也就是说,只要闭包的作用域链中保存着一个HTML元素,那么就意味着这个元素将无法被销毁。例如下面的:

  function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function(){
        alert (element.id);
   }; 
}

注意,闭包的概念是一个函数的返回值是另外一个函数,返回的那个函数如果调用了其父函数内部的其他值,这是一个element元素事件处理的一个闭包。这个闭包创建了一个循环引用。

  1. JavaScript对象element引用了一个DOM对象(其id为“someElement”); JS(element) ----> DOM(someElemet)
  2. 该DOM对象的onclick属性引用了匿名函数闭包,而闭包可以引用外部函数assignHandler 的整个活动对象,包括element ; DOM(someElement.onclick) ---->JS(element)
    匿名函数一直保存着对assginHandler()活动对象的引用,它所占的内存永远不会被回收。

可以对代码稍稍改进:

  function assignHandler(){
    var element = document.getElementById("someElement");
    var id = element.id;
    element.onclick = function(){
        alert (id);
   }; 
   element = null;
}

可以首先通过引用一个副本变量消除循环引用,但是这个闭包包含外部函数的全部活动对象,所以就算不直接引用,匿名函数一直都包含着对element的引用。所以最后需要手动设置element变量为null.

闭包的很大作用是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
只要变量被任何一个闭包使用了,就会被添到词法环境中,被该作用域下所有闭包共享。

绝对不能因为内存泄漏而不使用闭包。

JS中只要越过了闭包,作用域,原型这几座大山,对JS的理解就高了一个层次了!
作用域的文章可以查看下面的网址!!!https://lemontency.github.io/2019/03/20/%E5%8E%9F%E5%9E%8B%E4%B8%8E%E5%8E%9F%E5%9E%8B%E9%93%BE/

最后来几道作用域和this的题吧!

if(!(username in window)){
    var username = 'zty';
}
console.log(username) //undefined

怎么理解呢。首先在ES5的代码中并没有块级作用域的概念,var username直接变量提升到最顶部,此时全局作用域中有username并且值为undefined,所以!(username in window)为false,跳过赋值部分,username依旧是undefined。

//差点就错了
//执行顺序是
//声明函数foo,注意是整个函数一起提升上去的
//调用函数foo
//声明变量test
//console.log(test)
//test = "bbb"
//console.log(test)
var test = "aaa";
function foo(){
    console.log(test);
    var test = "bbb";
    console.log(test);
}
foo(); //undefined bbb 
console.log(test)//aaa

也就是说上面的代码的执行顺序其实是这样的:

var test = aaa;
function foo(){
    var test;
    console.log(test);
    var test = "bbb";
    console.log(test);
}
foo();
console.log(test);

var name = 'global';
function A(name){
    //传参的优先级要高于后面的声明
    //虽然后面有变量提升,但是是没有影响的
    alert(name);//3
    this.name = name;
    var name = '1';
}
A.prototype.name = '2';
var a = new A('3');
alert(a.name);//3
delete a.name;//删除对象的属性但是不删除对象原型链上的属性
alert(a.name);//2

比较好的实践就是把要用到的变量声明都写到函数最前面。
再来看一道容易做的题:

function fun(n,o){
    console.log(o);
    return {
        fun:function(m){
            return fun(m,n)
        }
    }
}
var a = fun(0);//undefined
a.fun(1);
a.fun(2);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1);
c.fun(2);c.fun(3);

我们看到:return返回的对象的fun属性对应一个新建的函数对象,这个函数对象将形成一个闭包作用域,使其能够访问外层函数的变量n及外层函数fun,为了不让fun属性和fun对象混淆,先更改一下代码:

function _fun_(n,o){
    console.log(o);
    return {
        fun:function(m){
            return _fun_(m,n)
        }
    }
}
var a = _fun_(0);//undefined
a.fun(1);//
a.fun(2);
var b = _fun_(0).fun(1).fun(2).fun(3);
var c = _fun_(0).fun(1);
c.fun(2);c.fun(3);

这道题难度挺大,但是一定要做。
讲师小tips:每次调用fun就相对应的在纸上还原作用域链。

_fun_函数执行,因为第2个参数未定义,输出undefined。然后返回一个对象,带有fun属性,指向一个函数对象,带有闭包,能够访问到_fun_和变量n。
a.fun(1)执行返回的对象的fun方法,传入m的值1,调用返回_fun_(1,0),打印出0;
a.fun(1)执行返回的对象的fun方法,传入m的值1,调用返回_fun_(2,0),打印出0;

接下来的var b = _fun_(0).fun(1).fun(2).fun(3);可以等价为

var b = _fun_(0);
var b1 = b.fun(1);
var b2 = b1.fun(2);
var b3 = b2.fun(3);
前两句的和我们刚刚分析的情况是一样的,var b = _fun_(0);首先返回一个对象,带有fun属性,指向一个函数对象,带有闭包,能够访问_fun_和变量n。
b.fun(1)传入m的值为1,调用返回_fun_(1,0),打印出0;
b1.fun(2)传入m的值为2,调用返回_fun_(2,1),打印出1;
b2.fun(3)传入m的值为3,调用返回_fun_(3,2),打印出2;

接下来var c = _fun_(0).fun(1);情况和前面的是一样的,先打印出undefined,再打印出0;
c.fun(2)传入m的值为2,调用返回_fun_(2,1),打印出1;
c.fun(3)传入m的值为,调用返回_fun_(3,1),打印出1;

还要注意,这里传递的是一个简单的数据类型,而不是引用的数据类型。
看看这个?
https://lemontency.github.io/2019/03/20/JS%E4%B8%AD%E7%9A%84%E6%8C%89%E5%80%BC%E4%BC%A0%E9%80%92%E5%92%8C%E6%8C%89%E5%BC%95%E7%94%A8%E9%97%AE%E9%A2%98-1/

参考:https://www.zhihu.com/question/48238548
https://segmentfault.com/a/1190000004187681

你可能感兴趣的:(JS语言精粹)