壹、构造函数
实例成员: 实例成员就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问
new一个新对象的过程,发生了什么?
(1) 创建一个空对象 son {} (2) 为 son 准备原型链连接 son.proto = Father.prototype (3) 重新绑定this,使构造函数的this指向新对象 Father.call(this) (4) 为新对象属性赋值 son.name (5) 返回this return this,此时的新对象就拥有了构造函数的方法和属性了
公共属性定义到构造函数里面,公共方法我们放到原型对象身上。
类和构造函数的区别
(1) 类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
(2) 类的所有实例共享一个原型对象。
(3) 类的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
构造函数特点:
1.构造函数有原型对象prototype。
2.构造函数原型对象prototype里面有constructor,指向构造函数本身。
3.构造函数可以通过原型对象添加方法。
4.构造函数创建的实例对象有__proto__原型,指向构造函数的原型对象。
类:
1.class本质还是function
2.类的所有方法都定义在类的prototype属性上
3.类创建的实例,里面也有__proto__指向类的prototype原型对象
4.新的class写法,只是让对象原型的写法更加清晰,更像面向对象编程的语法而已。
5.ES6的类其实就是语法糖。
ps:什么是语法糖?加糖后的代码功能与加糖前保持一致,糖在不改变其所在位置的语法结构的前提下,实现了运行时的等价。
语法糖没有改变语言功能,但增加了程序员的可读性。
贰、原型和原型链
(a) 原型
(a).1 什么是原型?
Father.prototype 就是原型,它是一个对象,我们也称它为原型对象。
(a).2 原型的作用是什么?
原型的作用,就是共享方法。 我们通过 Father.prototype.method 可以共享方法,不会反应开辟空间存储方法。
(a).3 原型中this的指向是什么?
原型中this的指向是实例。
(b) 原型链
(b).1 什么是原型链?
原型与原型层层相链接的过程即为原型链。
(b).2 原型链应用
对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在 每个对象都有__proto__原型的存在
function Peoples(name,age) {
this.name = name;
this.age = age;
}
Peoples.prototype.doiit = function(){
console.log('小帅哥过来玩啊',this.name);
};
let obj = new Peoples('张三',18);
console.log(obj.__proto__ === Peoples.prototype);//true
叁、作用域
作用域分为全局作用域、函数作用域以及es6新增的块级作用域。
1.全局作用域
顾名思义:能够在代码中任何地方都能访问到的对象拥有全局作用域,一般有三种情况的全局作用域:
1.1 window对象的属性拥有全局作用域
1.2 所有末定义直接赋值的变量自动声明为拥有全局作用域
1.3 在最外层定义的函数 和变量 都拥有全局作用域
全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会 污染全局命名空间, 容易引起命名冲突。这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。
2.函数作用域
函数作用域,是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
3.块级作用域
块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部
let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。
function getValue(boor) {
if (boor) {
let a = "blue";
return a;
} else {
// a 在此处不可用
return null;
}
// a 在此处不可用
}
- 禁止重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:
var a = 30;
let a = 40; // Uncaught SyntaxError: Identifier 'a' has already been declared
在本例中, a 变量被声明了两次:一次使用 var ,另一次使用 let 。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。
var num = 30;
// 不会抛出错误
if (true) {
let num = 40;
// 其他代码
}
- 循环中的绑定块作用域的妙用
我们可能最希望实现for循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例如:
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // ReferenceError: i is not defined
上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
4.作用域与执行上下文
许多开发人员经常混淆作用域和执行上下文的概念,误认为它们是相同的概念,但事实并非如此。
我们知道JavaScript属于解释型语言,JavaScript的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
作用域和执行上下文之间最大的区别是:
执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。