小小知识点:
-
typeof是一个操作符,并不是function。所以typeof后面不用加括号。
参见MDN
image.png -
关于 typeof null // Object
null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回"object"的原因,如 -
只声明不赋值,则默认值为undefined
-
关于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.
-
关于arguments,我们更推荐这么使用
这时的args就是真正的数组了。function foo(...args){ return args; } foo(1,2,3)//[1,2,3]
-
函数的返回值有两种结果:
显示调用return 返回 return 后表达式的求值
没有调用return 返回 undefined -
关于this
在普通函数中
严格模式下,this指向undefined
非严格模式下,this指向全局对象(Node中的global,浏览器中的window)
在ES6中默认使用严格模式,所以在普通函数中你会发现this指向undefined。 -
语句和表达式
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*/
逗号表达式用的地方不太多,一般情况是在给循环变量赋初值时才用得到。所以程序中并不是所有的逗号都要看成逗号运算符,尤其是在函数调用时,各个参数是用逗号隔开的,这时逗号就不是逗号运算符。
有时候会遇到一些很奇怪的错误,就尽量往语句优先这个方向想。
-
立即执行函数
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(){})() //强制其理解为函数,()里面放表达式,“函数()”表示执行该函数,即声明后立即执行了。 -
补充原型链的一个小知识:
每个原型对象prototype中都有一个constructor属性,默认指向函数本身。 -
闭包和内存泄露的问题
首先应当明确的是内存泄漏可能是代码的问题,而不是闭包的问题,如果为了避免内存泄漏而不去用闭包的话,有些问题是很难解决的。
在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元素事件处理的一个闭包。这个闭包创建了一个循环引用。
- JavaScript对象element引用了一个DOM对象(其id为“someElement”); JS(element) ----> DOM(someElemet)
- 该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
网友评论