Adding a tax_query to a WP_Query Object

If you instantiate WP_Query with arguments, as in your second example, then the query is performed right away, and setting new arguments will not change the results.

$q = new WP_Query( [ 'post_type' => 'post' ] );

$q->set( 'tax_query', [] ); // Too late.

If you instantiate WP_Query without arguments, like this:

$q = new WP_Query();

Then the query is not performed until $q->get_posts() is run. This means that you can set query vars after the object is created:

$q = new WP_Query();

$q->set( 'post_type', 'post' );
$q->set( 'tax_query', [] );

$q->get_posts();

while ( $q->have_posts() ) {
    // etc..
}

There’s nothing special about tax_query when it comes to setting arguments like this.

The pre_get_posts hook is fired inside WP_Query::get_posts(), before the query itself is performed, so in that hook there’s no reason you can’t set tax_query:

add_action(
    'pre_get_posts',
    function( $query ) {
        $query->set( 'tax_query', [] );
    }
);

The one thing that might trip you up is that any given query might already have a tax_query, and by setting it yourself you could be removing that original query. The way around this is to retrieve the original query, and then add yours.

add_action(
    'pre_get_posts',
    function( $query ) {
        $tax_query = $query->get( 'tax_query', [] ); // Make sure we have an array as the default.

        $new_tax_query = [
            [
                'taxonomy' => 'post_format',
                'field'    => 'slug',
                'operator' => 'IN',
                'terms'    => [ 'post-format-link' ],
            ],
            $tax_query,
        ];

        $query->set( 'tax_query', $new_tax_query );
    }
);