通过一道面试题来学习原型/原型链-函数声明/函数表达式

例题

如题:

答案解析
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
复制代码

参考


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值