作用域
问题
先看这段代码
1 | { |
是不是很不解???常规思路都是输出2啊。
没关系,在了解了JS块级作用域之后你就懂了
全局作用域和function
window
为全局对象,在任何一个地方,如果一个变量a = 1
(没有任何修饰,如var、let、const),那么也可以理解为window.a = 1
var
声明的变量的作用域在funtion
中,否则在上层的function,若上层没有function,那么就会延伸到window
中
eval('代码作用域'):默认eval执行的代码作用域同上。
但是如果在eval之前开启严格模式use strict;
那么eval里面的变量不会外溢。除非显示给外部对象赋值,如eval('window.bbb=48')
在Ecma5之前只有函数和全局作用域,也就是全局window
或者function(){...}
函数之内,而且var和function,在未声明之前可以访问,原因是js有内部变量提前的特性
在同一作用域下function
函数和var
声明的变量都会被提至当前作用域的顶层,var优先声明,function其次,其中function提升的同时,函数体的实现也定义了出来
1 | function test() { |
块级作用域
Ecma6新增了块级作用域,增加了两个变量修饰符:let
(值可变,不可二次声明)和const
(常量、值不可变,可二次声明),未声明之前访问会报错,而且var
和let
以及const
声明的变量不能互换
理解了函数和变量提升之后,那么问题来了,如果块级作用域和块外作用域共有一个同名的变量,而function函数写在块中,该函数引用到同名变量,那么该函数到底是用块内还是块外变量呢?如下
1 | var a = 1; |
很明显,按照变量和函数提升至顶层之后的思路,解释之后会输出1,可我们实际想要的是2呀。输出为1就违背了块级作用域的概念,那么该如何解决呢?毕竟变量和函数的提升是老的特性,新设计的特性肯定要兼容旧的。没办法,js制定者只能在做一些取舍了
取舍如下:
- 函数如果在块中,那么
funtion
和var
照样提升至前,只不过function的实现不允许提前定义,这样可以避免块中的内容溢出到块外,即块的内容只在块里面 - 解释器在解释到块级作用域时,如果块中有函数,那么会在块中的最初位置用
let
以及新的变量名,重新定义一下这个函数,因为funtion
被let
定义在块中了,那么该function肯定可以访问到块中的变量- 为什么会在块中用新的变量从新定义呢,因为一个变量不能从
var
和let
以及const
相互转换
- 为什么会在块中用新的变量从新定义呢,因为一个变量不能从
- 因为块内的函数的名称变了,所以块内涉及到的老的函数名称时,也要随着变。不然用
let
修饰的新变量名称也没有任何意义啊~ - 然后又因为函数的作用域不仅仅在块内,块外也可以访问(要兼容之前的特性),所以在执行到函数原有声明的位置时,他会用
var
以及原有的变量名再次声明一下
这样就解决了块外和块内的变量名一样的问题了。代码如下
1 | var a = 1; |
但是也有问题,比如在块中定义的函数,必须执行完块时,函数才可以访问
1
2
3
4
5
6
7 // test(); //执行会报错,找不到方法
{
function test() {
console.log(123);
}
}
test(); //执行完块时才可访问世界上没有任何东西是十全十美的,在一件大事件上要尽量争取最好的度,做出最大的兼容(成本最小,接受面最广)
答案
回到最初的问题,我们以新解释器的角度重新审视一下代码,就能彻底的理解作用域的概念啦
例子1
1 | { |
例子2
1 | a = 1; |
闭包
在块级作用域出现之前,只有函数作用域和全局作用域,为了解决变量的污染,就有了闭包,何为闭包?我理解的就是立即调用一个没有名字的函数,使其变量都在该函数中
(function(arg){console.log('我是闭包,arg:', arg)})(123/*把外部变量从这里传进去*/);
这样就把变量锁死在大括号中了,可以理解为对一个匿名方法的调用
也还有其他的写法如:!function(arg){console.log('我是闭包,arg:', arg)}(123/*把外部变量从这里传进去*/);
为什么前面必须有运算符呢?可以理解为一元运算符对后面匿名变量的运算,如果把感叹号
!
去掉的话,执行器就不知道后面到底是什么语法了
可以理解为只要是一元运算符后面都可以接匿名变量,+
或者-
运算符都可以都可以
为什么大多数都用!感叹号呢,因为运算时占用的cpu和空间比较少,他就是一个取反的运算:非真即假,其次因为编写也方便
对象
谈到对象,最熟悉的莫过于this
,在function中,this为谁调用我,我就是谁
1 | function Fun1(value){ |
JS方法中的this是可以改变的
test.apply(otherThis, arguments); // 参数必须传递数组
test.call(otherThis, ...arguments); // call只能把参数拆开, 而'...'语法就是用来拆参数的(就是用来脱衣服的)
test = test.bind(otherThis[,arg1[,arg2[, ...]]]); // 直接透明代理test方法,一劳永逸改变this
设计对象的结构
Ecma6之前,对象同方法,只需要new即可,至于对象的结构(包含的字段),在函数中用this,指定即可
1 | function test(arg1, arg2) { |
每个方法都有隐含的参数
arguments
类型为数组,该属性包含了该方法的所有参数,这个特性也就确定了js方法是没有重载的
原型链
- prototype
该属性对象上没有,仅为函数function所独有。
该属性作用就是让函数所实例化的对象们都可以找到公用的属性和方法
简称,实例的原型对象,通过实例的__proto__字段指向函数的prototype
- __proto__
该属性仅仅为对象所有,用来指向函数(该对象所属的function)的prototype
每个对象找某个属性时,查找范围除了自身之外,还会从__proto__属性中查找,直至找到或到顶层为止由于Function是个特殊,它既是对象也是函数
对象有 __proto__
函数有 prototype
所以Function.__proto__ == Function.prototype
以上两个关系如下代码所示
1
2
3 function Fun(){}
f1 = new Fun()
f1.__proto__ == Fun.prototype每一个函数独有prototype属性,每个对象独有__proto__属性(指向生成他函数的prototype,prototype的__proto__属性继续向上指向)
- constructor
构造函数,对应某个确切函数的化身,函数上的属性,方法都可在函数.prototype.constructor
找到
所以:函数.prototype.constructor
==函数本身
对象实例的__.proto__.constructor
==函数本身
对象实例的__.proto__.constructor.prototype
==函数.prototype
一般可以通过对象获取到constructor然后获取到对应的函数,然给在给对应的函数prototype新增新的字段或方法,从而给所有实例扩展新功能
原型链继承
了解了两个最重要的对象prototype
和__proto__
之后就知道该怎么继承了。
1 | function Person(name, age){ |
如果让张老李四做自我介绍呢?
1 | Person.prototype.toString = function () { |
很简单吧?
如何继承Person呢?
1 | function PersonSex(name, age, sex){ |
Object.defineProperty属性访问器
Ecma5就已经支持
如何给定某个属性代理get和set方法呢,如同Java那样有get和set呢?
get、set不能于value和writable同时存在(直接报错)
1 | let Person = {} |
proxy
ecma6才支持
如何给一个对象的所以属性增加set和get方法进行拦截呢? 用proxy
1 | var obj = []; |
class语法
Ecma6才支持
1 | class Animals{ |
异步
promise
1 | new Promise(function (resolve, reject) { |
setTimeout
1 | setTimeout(function () { |
setInterval
1 | var interval = setInterval(function () { |
多线程
Worker
意义在于可以将一些耗时的数据处理操作从主线程中剥离,使主线程更加专注于页面渲染和交互
使用方式如下
var worker = new Worker('work.js');
即可使用新线程执行另外一个js。但是有限制
- 执行新的js文件必须与主线程同源(不能跨域)
- worker全局对象与主线程不一样,无法使用dom、window对象,但是可以使用navigator和location对象
- 不能与主线程直接通信,必须通过事件消息的方式通讯。(消息最终都会序列化,所以也无法通过改对象里面的值使其双向通信)
- 不能使用alert、confirm等阻塞主线程的方法,但是可以使用XMLHttpRequest发起ajax请求
- 无法读取本地文件,他所加载的脚本或文件必须来自网络或者本地网络。
本地网络如:
1
2
3
4
5
6
7
8
9
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>1
2
3var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);