InterviewMap —— Javascript (一)
- 目录
- 全局对象
- 处理URL
- 面向对象
- 此法作用域
- 针对js执行宏观整体分析的总结
复制代码
一、全局对象
当js解析器启动(页面加载的时候),一些内置对象也会被初始化,比如: 全局对象、Object、String、Number、Boolean、Math、Data、Array等。
其中我们必须要注意的就是全局对象。
在创建一个全局对象的时候,会给他定义一些属性和方法,比如:
全局属性 | NaN、undefined、Infinity |
---|---|
全局方法 | eval() ParseInt() parseFLoat() isNaN() |
构造函数 | Object() Array() String() Number() Date() |
处理URL | escape() encodeURL decodeURL encodeURLComponent ... |
全局对象 | Math JSON |
JS的原声对象和宿主对象
原生对象就是ESMASCRIPT规定的那些对象,所有的内置对象都是原生对象
宿主对象就是宿主环境提供的一些对象,为了完善js的执行环境
复制代码
浏览器所规定的全局对象是Window对象。window对象中就包含了js的原生对象和宿主对象(document、navigator)
二、处理URL
编码函数 | 解码函数 |
---|---|
escape | unescape |
encodeURI | decodeURI |
encodeURLComponent | decodeURIComponent |
1、escape
简单来说,escape是对字符串(string)进行编码(而另外两种是对URL),作用是让它们在所有电脑上可读。这个方法是针对字符串使用的,不适用于URL。
编码之后的效果是%XX或者%uXXXX这种形式。
其中 ASCII字母、数字、@*/+ ,这几个字符不会被编码,其余的都会。
最关键的是,当你需要对URL编码时,请忘记这个方法,这个方法是针对字符串使用的,不适用于URL。
事实上,这个方法我还没有在实际工作中用到过,所以就不多讲了。
复制代码
2、最常用的encodeURI和encodeURIComponent
对URL编码是常见的事,所以这两个方法应该是实际中要特别注意的。
它们都是编码URL,唯一区别就是编码的字符范围
其中encodeURI方法不会对下列字符编码 ASCII字母、数字、~!@#$&*()=:/,;?+'
encodeURIComponent方法不会对下列字符编码 ASCII字母、数字、~!*()'
复制代码
所以encodeURIComponent比encodeURI编码的范围更大。实际例子来说,encodeURIComponent会把 http:// 编码成 http%3A%2F%2F 而encodeURI却不会。
3、最重要的,我该什么场合用什么方法
区别上面说的很清楚了,接下来从实际例子来说说把。
-
1、如果只是编码字符串,不和URL有半毛钱关系,那么用escape。
-
2、如果你需要编码整个URL,然后需要使用这个URL,那么用encodeURI。
比如
encodeURI("http://www.cnblogs.com/season-huang/some other thing");
编码后会变为
"http://www.cnblogs.com/season-huang/some%20other%20thing";
复制代码
其中,空格被编码成了%20。但是如果你用了encodeURIComponent,那么结果变为
"http%3A%2F%2Fwww.cnblogs.com%2Fseason-huang%2Fsome%20other%20thing"
复制代码
看到了区别吗,连 "/" 都被编码了,整个URL已经没法用了。
- 3、当你需要编码URL中的参数的时候,那么encodeURIComponent是最好方法。
var param = "http://www.cnblogs.com/season-huang/"; //param为参数
param = encodeURIComponent(param);
var url = "http://www.cnblogs.com?next=" + param;
console.log(url) //"http://www.cnblogs.com?next=http%3A%2F%2Fwww.cnblogs.com%2Fseason-huang%2F"
// 看到了把,参数中的 "/" 可以编码,如果用encodeURI肯定要出问题,因为后面的/是需要编码的。
复制代码
总结:
1、 如果你要处理整个url那么使用encodeURI
2、 如果你要处理整个字符串,对其进行编码,那就选择escape
3、 如果你要处理url传入的参数query,那么用encodeURLComponent
复制代码
三、对象
1、创建对象
- 使用 Object 构造函数创建
// 对象实例的创建
var obj = new Object()
obj.key = 'value' //使用构造函数创建一个空对象,并赋值
复制代码
- 使用对象字面量表示法创建
//使用字面量创建一个对象
var obj = {
key1: 'value1',
key2: 'value2'
}
复制代码
- ES6中还有更简洁的
var age = 20
var sex = "sexy"
var a = {
name: 'jack',
// 简洁表示法,等同于 age: age
age,
// 简洁表示法,等同于 sayName: function() {}
sayName(){},
// 属性名表达式,等同于 lover: 'rose'
['lo' + 'ver']: 'rose',
// 属性名表达式,等同于 sexy: 'male'
[sex]: 'male'
}
复制代码
2、工厂模式[创建多个相似的对象]
var createPerson (name, age){
var o = {};
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this);
}
return o;
}
var a = createPerson ('zjj', 20);
var b = createPerson ('zmf', 30);
复制代码
工厂模式虽然解决多创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
3、模仿“类”的设计
构造函数模式
function Person (name, age) {
this.name = name;
thia.age = age;
this.sayName = function() { alert(this.age) }
}
Person.prototype.count = 2;
var a = new Person('a', 20)
var b = new Person('b', 22)
a instanceof Person // true
复制代码
构造函数与其他函数唯一的区别就在于调用他们的方式不同。任何函数只要通过new 操作符来调用,那它就可以作为构造函数。
4、new的模拟实现
使用new操作符调用函数
function CO(){
this.p = “I’m in constructed object”;
this.alertP = function(){
alert(this.p);
}
}
var o2 = new CO();
// new 的过程:
// 1、首先实例化一个对象 var obj = {}
// 2、obj.__proto__ = CO.prototype 第一步,该对象的原型链指向构造函数的 prototype 所指向的对象。
// 3、CO.call(obj) 第三步,将构造函数的作用域赋值给新的对象
// 4、return obj 返回新的对象
复制代码
自己实现一个new
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数, // 获取第一个参数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return obj
}
function Foo(){this.name= "zjj"}
create(Foo);
// 解释:
1 用new Object() 的方式新建了一个对象 obj
2 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
3 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
4 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
5 返回 obj
复制代码
上面这种构造函数解决了对象类型识别的问题,但是每个方法都要在每个实例上重新创建一遍,在上面的例子中,a 和 b 都有个名为sayName()的方法,这两个方法虽然名字、内容、功能相同,但却分别在 a 和 b 中都重新创建了一次,这是没有必要的。
更好的方法应该是将公用的方法放到他们的原型上,也就是接下来要说的原型模式。
5、原型模式
所有函数都有一个不可枚举的 prototype(原型)属性,这个属性是一个指针,指向一个对象。
Person.prototype // Person的原型对象
Person.prototype.constructor == Person
p.__proto__ == Person.prototype
复制代码
^深入分析
第一步 分析上图
foo 是一个构造函数
fooObj 是foo实例化之后的对象
foo.prototype 是foo的原型对象
fooObj.__proto__只想foo的原型对象,即 foo.__proto__ == foo.prototype
foo.prototype中含有一个属性为constructor,指向了foo
复制代码
第二步 proto 和 prototype的区别
__proto__ 是对象的属性,指向其构造器原型对象
prototype 每一个函数都有,是构造器原型对象
复制代码
总之: __proto__是对象所有,prototype 是函数所有。constructor为prototype对象所有。
第三步 Person.proto == Functioin.prototype
Person是一个构造函数,同时也是一个对象,他也存在__proto__,指向Function.prototype
第四步 弄清楚__proto__的指向
6、原型链
为什么会有原型链,因为每一个对象都有__proto__,而js是万物皆对象,因此就形成了一条由__proto__连接的链条。
通过分析以下两图,相信我们都会有更深的认识了~~
总结:
1、 每个函数都有prototype属性,除了Function.prototype.bind()
2、 每一个对象都有一个__proto__属性,指向了创建该对象的构造函数的原型。其实这个属性指向的是[[prototype]],只是我们不能访问到,所以我们使用__proto__来访问。
3、 对象可以通过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象连接起来组成了原型链
4、 函数的 prototype 是一个对象,也就是原型
5、 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链
复制代码
7、Instanceof
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
我们也可以试着实现一下 instanceof
function instanceof(left, right) {
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
复制代码
上面我们了解了面向对象的一些知识,现在我们来针对js引擎如何解析和执行我们的代码来分析一下: 首先是词法作用域
四、词法作用域
1、作用域
作用域规定了我们所定义的变量的作用范围。它规定了我们如何在作用域中查询变量,已经当前代码执行时对变量的访问权限。
js采用的是词法作用域,即静态作用域。ES5 有函数作用域,全局作用域。ES6新增了块级作用域。
2、静态作用域和动态作用域
js是此法作用域,在函数定义好的时候,作用域就已经被定好了,所以是静态作用域,与之相反的是动态作用域,动态作用域是在代码执行的时候确定的。
栗子:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 ???
复制代码
假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。
前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。
复制代码
让我们看一个《JavaScript权威指南》中的例子:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope; // 这里的scope一定的是使用该函数所在作用域链上的scope
}
return f;
}
checkscope()();
复制代码
猜猜两段代码各自的执行结果是多少?
这里直接告诉大家结果,两段代码都会打印:local scope
。
原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。
有了词法作用域,我们就把整个代码程序分为一个大的全局作用域以及其内部的许多的小的函数作用域。那么综合这些不同的域,我们的具体代码是怎么在里面执行,以及它的整个执行顺序是什么呢?
五、针对js执行宏观整体分析的总结:
首先我们加载代码,整个程序的执行可以分为两个阶段,一个是预处理阶段
、一个是代码执行阶段
。细分的化还可以分为全局预处理阶段
和全局执行阶段
,以及函数预处理阶段
和函数执行阶段
。
1、预处理阶段
首先js是静态作用域,划分全局作用域和函数(局部作用域)。在函数和变量的定义的过程中就已经确定好了作用域。
第一步 执行上下文栈的处理
为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)
的概念。也就是说,当JavaScript
代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈
(Execution context stack,ECS)。
所以我们认识了执行上下文和执行上下文栈两个概念。执行上下文栈就是用来管理执行上下文的。
放一段代码:
var a = "global var";
function foo(){
console.log(a);
}
function outerFunc(){
var b = "var in outerFunc";
console.log(b);
function innerFunc(){
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc()
复制代码
代码首先进入Global Execution Context
,然后依次进入outerFunc
,innerFunc
和foo
的执行上下文,执行上下文栈就可以表示为:
看左侧的箭头,向下代表着入栈操作,执行上下文栈中会依次入栈 入Global Execution Context
,outerFunc
,innerFunc
和foo
。向上的箭头表示的就是执行阶段了,我们先不看。
第二步 变量对象的初始化和完善
对于每个Execution Context
都有三个重要的属性,变量对象(Variable object,VO)
,作用域链(Scope chain)
和this
。
我们要首先认识变量对象VO:
变量对象是与执行上下文相关的变量的作用域范围,记录了该执行上下文中所定义的变量和函数。
因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。
变量对象中一般包括:
变量 (var, Variable Declaration);
函数声明 (Function Declaration, FD);
函数的形参
复制代码
全局上下文
在全局的情况下,顶层对象就是全局对象,最为全局函数、和全局属性的占位符,全局中可以用this引用全局对象,window也指向全局对象。
全局上下文中的变量对象就是全局对象。
上面栗子:
VO = {
a: 'global var',
foo: <function>
outerFunc: <function>
}
复制代码
下面的情况并不会写入Vo中。
(function bar(){}) // function expression, FE
baz = "property of global object"
复制代码
函数上下文
在函数中我们用活动对象(AO)来代替变量对象(VO)。
活动对象(AO) 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。
对于VO和AO的关系可以理解为,VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO;但是,在函数Execution Context中,AO就会被创建。
上面OuterFun中的AO如下:
AO = {
arguments: {},
b : 'var in outerFunc',
innerFunc: <function>
}
复制代码
第三步 作用域的确定
我们知道当我们执行某一段代码是时候,就会创建对应的执行上下文,作用域链也是非常重要的一部分。
执行上下文中会创建变量对象的一个作用域链(scope chain)
来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。
函数内部有一个属性为[[scope]],函数被创建的时候就会产生这个属性。你可以理解为这个属性是其父级的变量对象的一个层级链。指向父级的变量对象。
function foo() {
function bar() {
...
}
}
复制代码
当foo被创建的时候和bar被创建的时候:
foo.[[scope]] = {
globalContext: VO
}
bar.[[scope]] = {
fooContext.AO,
globalContext: VO
}
复制代码
我们知道了[[scope]]那么scope是什么呢?
scope正是我们的变量对象中的作用域链的表示。
我们使用栗子来分析
var scope = 'global scope';
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
复制代码
它的执行过程如下:
【1】第一步 全局变量对象VO的创建
VO = {
scope: 'undefined',
checkscope: function(){}
}
复制代码
【2】第二步 函数创建阶段
// 创建[[scope]]
checkscope.[[scope]] = {
globalContext.VO
}
复制代码
【3】第三步 执行函数,创建checkscope的执行上下文
// 函数执行上下文创建好后,压入执行上下文栈中
ECStack = [
checkscopeContext,
globalContext
];
复制代码
【4】第四步 函数并不会立马执行,首先是创建函数的scope
checkscopeContext: {
scope: checkscope.[[scope]]
}
复制代码
【5】第五步 创建函数的激活对象AO
checkscopeContext: {
scope: checkscope.[[scope]],
AO: {
arguments: {
length: 0
},
scope2: undefined
}
}
复制代码
【6】将AO压入到作用域链中
checkscopeContext: {
scope: [AO, checkscope.[[scope]]],
AO: {
arguments: {
length: 0
},
scope2: undefined
}
}
复制代码
【7】到此函数执行上下文对象创建完毕,准备工作就绪,开始执行
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, checkscope.[[scope]]]
}
复制代码
【8】内部函数执行完毕之后,函数执行上下文就弹出栈中
ECStack = [
globalContext
];
复制代码
第四步 this的绑定
this是一个对象,他是一个对象的内部对象,随着函数场合的不同而不同。
this指向什么,完全取决于 什么地方以什么方式调用,而不是 创建时。
具体的this绑定我们单独拿出来温习一下,因为这块确实是让很多人都头疼的地方。
2、执行阶段
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。
在执行阶段就是引擎通过分析代码,设置变量对象的值,也就是作用域内的值,来相应的调用和执行相关变量。
来两个栗子吧:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
复制代码
在预处理阶段:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: function(){},
d: undefined
}
复制代码
执行阶段:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
复制代码
来几道题吧:
【1】例题
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
复制代码
解答:
第一段会报错:Uncaught ReferenceError: a is not defined。
第二段会打印:1。
第一种:
AO = {
arguments:{
length: 0
}
}
在AO中并没有a,所以报错
第二种:
AO = {
arguments:{
length: 0
}
}
因为在console之前出现了 a=1; 将a变成了全局变量了,在调用内部函数的时候,通过作用域链访问的是全局的a。
复制代码
总之: 预处理阶段用于创建执行上下文对象(变量对象、this、scope),并压入到执行上下文栈中的过程。执行阶段就是使用this和scope,并且改变变量对象,和使用变量对象以及执行上下文对象弹出的过程。