例题
如题:
答案解析
function A() {
B = function () {console.log(10)}
return this
};
A.B = function () {console.log(20)};
A.prototype.B = function () {console.log(30)}
var B = function () {console.log(40)}
function B() {console.log(50)}
A.B() // answer 20 【原型与原型链】
// 在`A`的原型对象中查找是否有`B`函数并且调用,这里并未执行`A`函数。
// A.B = function () {console.log(20)};
// 在A的原型对象中添加了`B`函数,停止查找,所以答案为 20
B() // answer 40 【函数表达式和函数声明】
// var B = function () {console.log(40)}
// function B() {console.log(50)}
// 同名 -> 函数提升会 > 变量提升
// 换言之 同名的函数表达式和函数声明同时存在时 总是执行表达式
A().B() // answer 10 【函数表达式和函数声明】
// A() 执行函数A ==> 1.变量B重新赋值函数 2.返回this(window)
// .B() 执行全局下的B函数 已经被重新赋值 所以输出10
B() // answer 10
// 上面的代码执行过A函数了,此时全局下的B函数输出10
new A.B() // answer 20【函数表达式和函数声明】
// new 执行了 A.B = function () {console.log(20)};
new A().B() // answer 30
// new 执行构造函数 A ,全局变量 B 重新赋值函数10
// 此时A() 指针指向哪里?
// 首先要知道 new 做了什么事?
// ==> 创建空对象objA objA.__proto__ = A.prototype
// .B() 在A的原型对象中查找 B; A.prototype 指向函数的原型对象
// A.prototype.B = function () {console.log(30)} 输出 30
复制代码
原型和原型链
prototype
每一个函数都有一个
prototype
属性。
function Foo() {}
Foo.prototype; // {constructor,__proto__}
复制代码
无论什么时候,只要创建了一个新函数,根据一组特定的规则为该函数创建一个prototype 属性,这个属性指向函数的原型对象。
那么这个创建的原型对象是什么呢?
{
constructor: ƒ Foo(),
__proto__: Object
}
复制代码
constructor 属性
每一个原型对象都有一个
constructor
属性
创建了自定义的构造函数后,其原型对象只会默认取得 constructor
属性。这个属性解决了对象识别问题,即可以通过该属性判断出实例是由哪个构造函数创建的。
Foo.prototype.constructor === Foo; // true
复制代码
前面说了,原型对象只会默认取得 constructor
属性,那么原型对象的其他属性(比如:__proto__
)是这么来的呢,这就要说到 __proto__
指针了。
proto
每一个实例都有一个
__proto__
指针,指向构造函数的原型对象。
var foo = new Foo();
foo.__proto__ === Foo.prototype; //true
复制代码
上面提到的构造函数的原型对象它本身也是一个实例,所以在它内部会有一个__proto__
指针。
构造函数(补充)
ECMAScript
中提供了构造函数来创建新对象。但构造函数本身就是一个函数,与普通函数没有任何区别,只不过为了区分,一般将其首字母大写,但这并不是必须的。
函数被 new 关键字调用时就是构造函数。
function f(name) {
console.log("execute");
this.name = name;
}
var k = new f("k"); // execute =====> 调用new
console.log(k); // {name: "k"}
var h = f("h"); // execute =====> 未调用new
console.log(h); // undefined
复制代码
从上面代码可以看出:
- 首字母是否大写并不影响函数 f 作为构造函数使用,
- 不使用 new 调用函数就是普通函数,直接执行内部代码,使用
new
,函数的角色就成为了构造函数,创建一个对象并返回。
对象由构造函数通过 new 创造对象的步骤
var obj = {}; // 创建一个空对象
obj.__proto__ = constructor.prototype;//添加__proto__属性,并指向构造函数的prototype 属性。
constructor.call(this); // 绑定this
return obj;
复制代码
new 关键字的内部实现机制:
- 创建一个新对象;
- 将构造函数的作用域赋值给新对象;
- 执行构造函数中的代码;
- 返回新对象
原型链
原型链的理论主要基于上述提到的构造函数、实例和原型的关系:
- 每一个构造函数都有一个原型对象
- 原型对象都包含一个指向构造函数的
constructor
属性 - 每一个实例都包含一个指向原型对象的
__proto__
指针 其中最最重要的是第三条,依赖这条关系,层层递进,就形成了实例与原型的链条。
接着上面的探索,构造函数的原型的原型是由 Object
生成的,那么 Object
的原型是由什么生成?而原型链的终点又是在哪?
Object.prototype.__proto__ // null
null.__proto__; // Uncaught TypeError: Cannot read property '__proto__' of null
// game over
复制代码
原型的终点是 null
,因为 null
没有 proto
属性。
最后以一个例子来理解上面所谈到的原型与原型链
function Foo(){} // 构造函数 Foo
var foo = new Foo() // foo.__proto__ 指向 Foo.prototype
复制代码
函数声明、函数表达式
函数声明 function name(){}
函数声明是用指定的参数声明一个函数。一个被函数声明创建的函数是一个 Function
对象,具有 Function
对象的所有属性、方法和行为。
// 函数声明语法
function name([param[, param[, ... param]]]) { statements }
复制代码
函数表达式 var name = function(){}
在函数表达式中我们可以忽略函数名称创建匿名函数,并将该匿名函数赋值给变量。
var add = function(a, b) {
return a + b;
};
add(2, 3) // 5
复制代码
当然, 也可以创建命名函数表达式 Named function expression:
var add = function func(a, b) {
return a + b;
};
add(2, 3) // 5
复制代码
命名函数表达式中函数名称只能作为函数体作用域内的局部变量,外部不可访问。
var a = function pp(v) {
v++;
if (v>3) {
return v;
} else {
return pp(v);
}
}
a(1); // 4
pp(1); // ReferenceError: pp is not defined
复制代码
函数声明提升
对于函数声明创建的函数,可以在本作用域内任意位置访问。
a(); // 1
function a() {
return 1;
}
a(); // 1
复制代码
而函数表达式不会。
console.log(a); // undefined (只是变量提升)
a(1); // TypeError: a is not a function
var a = function(v) {
console.log(v);
};
复制代码
函数提升和变量提升的疑惑分析
console.log(fn); // [Function: fn]
var fn = function () {
console.log(1);
}
function fn() {
console.log(2);
}
fn() // 1
复制代码
提升过程
// 函数提升
function fn() {
console.log(2);
}
// 变量提升
var fn;
fn = function () {
console.log(1);
}
fn() //最终输出1
复制代码